diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b62a8f20eadc475ab5a95b30e9a6eb2b2e345d..b5a0e729877076c966b2ffa207122ea46032b2bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,23 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## ### Added ### -* `ParentList` and `PropertyList` now have a `filter` function that allows to select a subset of - the contained elements by ID and/or name. * Official support for Python 3.13 * Optional `realm` argument for `linkahead_admin.py set_user_password` which defaults to `None`, i.e., the server's default realm. ### Changed ### -* `compare_entities` is now case insensitive with respect to property and - recordtype names -* `_ParentList` is now called `ParentList` -* `_Properties` is now called `PropertyList` -* `ParentList.remove` is now case insensitive when a name is used. ### Deprecated ### -* the use of the arguments `old_entity` and `new_entity` in `compare_entities` - is now deprecated. Please use `entity0` and `entity1` respectively instead. ### Removed ### diff --git a/src/doc/tutorials/complex_data_models.rst b/src/doc/tutorials/complex_data_models.rst index b33bc9ae49e9a850a73e32e396735652ecaab7d7..569acdae174a9df9d0d2b5eae9a0084d793cc90c 100644 --- a/src/doc/tutorials/complex_data_models.rst +++ b/src/doc/tutorials/complex_data_models.rst @@ -75,58 +75,3 @@ Examples b = input("Press any key to cleanup.") # cleanup everything after the user presses any button. c.delete() - - -Finding parents and properties --------- -To find a specific parent or property of an Entity, its -ParentList or PropertyList can be filtered using names, ids, or -entities. A short example: - -.. code-block:: python3 - - import linkahead as db - - # Setup a record with four properties - r = db.Record() - p1 = db.Property(id=101, name="Property 1") - p2 = db.Property(id=102, name="Property 2") - p3_1 = db.Property(id=103, name="Property 3") - p3_2 = db.Property(id=103, name="Property 3") - p4 = db.Property(name="Property") - p5 = db.Property(name="Property") - r.add_property(p1).add_property(p2).add_property(p3_1) - r.add_property(p3_2).add_property(p4).add_property(p5) - properties = r.properties - - # As r only has one property with id 101, this returns a list containing only p1 - properties.filter(pid=101) - # Result: [p1] - - # Filtering with name="Property" returns both p4 and p5, as they share their name - properties.filter(name="Property") - # Result: [p4, p5] - - # Filtering with name="Property 1" and id=102 returns both p1 and p2, because - # any property matching either criterion is returned: - properties.filter(name="Property 1", pid="102") - # Result: [p1, p2] - - p6 = db.Property(name="Property 2") - r.add_property(p6) - # If we want to find properties matching one specific property, we can also filter using - # the entity itself. In this case, only properties matching both name and id are returned, - # as long as both are set. - properties.filter(p2) - # Result: [p2] - # As p6 does not have an id yet, both candidates matching its name are returned - properties.filter(p6) - # Result: [p2, p6] - # Similarly if we match using name and id parameters, all candidates matching either are returned - properties.filter(name=p2.name, pid=p2.id) - # Result: [p2, p6], because p2 and p6 share a name - # And if both name and id match, there may also be several results when matching an entity - properties.filter(p3_1) - # Result: [p3_1, p3_2], because they share both their name and id - -The filter function of ParentList works analogously. diff --git a/src/linkahead/__init__.py b/src/linkahead/__init__.py index 567748e3b3a58fb73b91f652d82ed10f818d6014..cd54f8f4e05326579521fbbf226f027d32fa616e 100644 --- a/src/linkahead/__init__.py +++ b/src/linkahead/__init__.py @@ -42,7 +42,7 @@ from .common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, LIST, REFERENCE, TEXT) # Import of the basic API classes: from .common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED, - SUGGESTED, Container, DropOffBox, Entity, File, Parent, + SUGGESTED, Container, DropOffBox, Entity, File, Info, Message, Permissions, Property, Query, QueryTemplate, Record, RecordType, delete, execute_query, get_global_acl, diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py index 8293fd9f6d4fb996c146f1559a0c6a714beddcfc..4ae8edd16f1fdc00eb7ba2c17661eea6e114885e 100644 --- a/src/linkahead/apiutils.py +++ b/src/linkahead/apiutils.py @@ -28,11 +28,10 @@ """ from __future__ import annotations - import logging import warnings from collections.abc import Iterable -from typing import Any, Optional, Union +from typing import Any, Union, Optional from .common.datatype import is_reference from .common.models import (SPECIAL_ATTRIBUTES, Container, Entity, File, @@ -180,273 +179,167 @@ def getCommitIn(folder): return get_commit_in(folder) -def compare_entities(entity0: Optional[Entity] = None, - entity1: Optional[Entity] = None, - compare_referenced_records: bool = False, - entity_name_id_equivalency: bool = False, - old_entity: Optional[Entity] = None, - new_entity: Optional[Entity] = None, +def compare_entities(old_entity: Entity, + new_entity: Entity, + compare_referenced_records: bool = False ) -> tuple[dict[str, Any], dict[str, Any]]: - """Compare two entities. - - Returns two dicts listing the differences between the two entities. The - order of the two returned dicts corresponds to the two input entities. - The dicts contain two keys, 'parents' and 'properties'. The list saved - under the 'parents' key contains those parents of the respective entity - that are missing in the other entity, and the 'properties' dict contains - properties and SPECIAL_ATTRIBUTES if they are missing or different from - their counterparts in the other entity. - - The value of the properties dict for each listed property is again a dict - detailing the differences between this property and its counterpart. - The characteristics that are checked to determine whether two properties - match are the following: + """Compare two entites. + + Return a tuple of dictionaries, the first index belongs to additional information for old + entity, the second index belongs to additional information for new entity. + + Additional information means in detail: + - Additional parents (a list under key "parents") + - Information about properties: + - Each property lists either an additional property or a property with a changed: - datatype - - importance - - value - If any of these characteristics differ for a property, the respective - string (datatype, importance, value) is added as a key to the dict of the - property with its value being the characteristics value, - e.g. {"prop": {"value": 6, 'importance': 'SUGGESTED'}}. - If a property is of type LIST, the comparison is order-sensitive. - - Comparison of multi-properties is not yet supported, so should either - entity have several instances of one Property, the comparison is aborted - and an error is raised. - - Two parents match if their name and id are the same, any further - differences are ignored. - - Should records referenced in the value field not be checked for equality - between the entities but for equivalency, this is possible by setting the - parameter compare_referenced_records. - - Params - ------ - entity0 : Entity - First entity to be compared. - entity1 : Entity - Second entity to be compared. - compare_referenced_records: bool, default: False - If set to True, values with referenced records - are not checked for equality but for - equivalency using this function. - compare_referenced_records is set to False for - these recursive calls, so references of - references need to be equal. If set to `False`, - only the Python objects are compared, which may - lead to unexpected behavior. - entity_name_id_equivalency: bool, default: False - 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". + - importance or + - value (not implemented yet) + + In case of changed information the value listed under the respective key shows the + value that is stored in the respective entity. + + If `compare_referenced_records` is `True`, also referenced entities will be + compared using this function (which is then called with + `compare_referenced_records = False` to prevent infinite recursion in case + of circular references). + + Parameters + ---------- + old_entity, new_entity : 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 + object as value. If set to `False`, only the corresponding Python + objects are compared which may lead to unexpected behavior when + identical records are stored in different objects. Default is False. + """ - # ToDo: Discuss intended behaviour - # Questions that need clarification: - # - What is intended behaviour for multi-properties and multi-parents? - # - Do different inheritance levels for parents count as a difference? - # - Do we care about parents and properties of properties? - # - Should there be a more detailed comparison of parents without id? - # - Revisit filter - do we care about RecordType when matching? - # How to treat None? - # Suggestions for enhancements: - # - For the comparison of entities in value and properties, consider - # keeping a list of traversed entities, not only look at first layer - # - Make the empty_diff functionality faster by adding a parameter to - # this function so that it returns after the first found difference? - # - Add parameter to restrict diff to some characteristics - 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 - - diff: tuple = ({"properties": {}, "parents": []}, - {"properties": {}, "parents": []}) - - if entity0 is entity1: - return diff - - if type(entity0) is not type(entity1): + olddiff: dict[str, Any] = {"properties": {}, "parents": []} + newdiff: dict[str, Any] = {"properties": {}, "parents": []} + + if old_entity is new_entity: + return (olddiff, newdiff) + + if type(old_entity) is not type(new_entity): raise ValueError( "Comparison of different Entity types is not supported.") - # compare special attributes for attr in SPECIAL_ATTRIBUTES: - if attr == "value": + try: + oldattr = old_entity.__getattribute__(attr) + old_entity_attr_exists = True + except BaseException: + old_entity_attr_exists = False + try: + newattr = new_entity.__getattribute__(attr) + new_entity_attr_exists = True + except BaseException: + new_entity_attr_exists = False + + if old_entity_attr_exists and (oldattr == "" or oldattr is None): + old_entity_attr_exists = False + + if new_entity_attr_exists and (newattr == "" or newattr is None): + new_entity_attr_exists = False + + if not old_entity_attr_exists and not new_entity_attr_exists: continue - attr0 = entity0.__getattribute__(attr) - # we consider "" and None to be nonexistent - attr0_unset = (attr0 == "" or attr0 is None) + if ((old_entity_attr_exists ^ new_entity_attr_exists) + or (oldattr != newattr)): - attr1 = entity1.__getattribute__(attr) - # we consider "" and None to be nonexistent - attr1_unset = (attr1 == "" or attr1 is None) + if old_entity_attr_exists: + olddiff[attr] = oldattr - # in both entities the current attribute is not set - if attr0_unset and attr1_unset: - continue + if new_entity_attr_exists: + newdiff[attr] = newattr + + # properties + + for prop in old_entity.properties: + matching = [p for p in new_entity.properties if p.name == prop.name] - # treat datatype separately if one datatype is an object and the other - # a string or int, and therefore may be a name or id - if attr == "datatype": - if not attr0_unset and not attr1_unset: - if isinstance(attr0, RecordType): - if attr0.name == attr1: - continue - if str(attr0.id) == str(attr1): - continue - if isinstance(attr1, RecordType): - if attr1.name == attr0: - continue - if str(attr1.id) == str(attr0): - continue - - # add to diff if attr has different values or is not set for one entity - if (attr0_unset != attr1_unset) or (attr0 != attr1): - if not attr0_unset: - diff[0][attr] = attr0 - if not attr1_unset: - diff[1][attr] = attr1 - - # compare value - ent0_val, ent1_val = entity0.value, entity1.value - if ent0_val != ent1_val: - same_value = False - - # Surround scalar values with a list to avoid code duplication - - # this way, the scalar values can be checked against special cases - # (compare refs, entity id equivalency etc.) in the list loop - if not isinstance(ent0_val, list) and not isinstance(ent1_val, list): - ent0_val, ent1_val = [ent0_val], [ent1_val] - - if isinstance(ent0_val, list) and isinstance(ent1_val, list): - # lists can't be the same if the lengths are different - if len(ent0_val) == len(ent1_val): - lists_match = True - for val0, val1 in zip(ent0_val, ent1_val): - if val0 == val1: - continue - # Compare Entities - if (compare_referenced_records and - isinstance(val0, Entity) and isinstance(val1, Entity)): - try: - same = empty_diff(val0, val1, False, - entity_name_id_equivalency) - except (ValueError, NotImplementedError): - same = False - if same: - continue - # Compare Entity name and id - if entity_name_id_equivalency: - if (isinstance(val0, Entity) - and isinstance(val1, (int, str))): - if (str(val0.id) == str(val1) - or str(val0.name) == str(val1)): - continue - if (isinstance(val1, Entity) - and isinstance(val0, (int, str))): - if (str(val1.id) == str(val0) - or str(val1.name) == str(val0)): - continue - # val0 and val1 could not be matched - lists_match = False - break - if lists_match: - same_value = True - - if not same_value: - diff[0]["value"] = entity0.value - diff[1]["value"] = entity1.value - - # compare properties - for prop in entity0.properties: - matching = entity1.properties.filter(prop, check_wrapped=False) if len(matching) == 0: - # entity1 has prop, entity0 does not - diff[0]["properties"][prop.name] = {} + olddiff["properties"][prop.name] = {} elif len(matching) == 1: - diff[0]["properties"][prop.name] = {} - diff[1]["properties"][prop.name] = {} - propdiff = (diff[0]["properties"][prop.name], - diff[1]["properties"][prop.name]) - - # We should compare the wrapped properties instead of the - # wrapping entities if possible: - comp1, comp2 = prop, matching[0] - if (comp1._wrapped_entity is not None - and comp2._wrapped_entity is not None): - comp1, comp2 = comp1._wrapped_entity, comp2._wrapped_entity - # Recursive call to determine the differences between properties - # Note: Can lead to infinite recursion if two properties have - # themselves or each other as subproperties - od, nd = compare_entities(comp1, comp2, compare_referenced_records, - entity_name_id_equivalency) - # We do not care about parents and properties here, discard - od.pop("parents") - od.pop("properties") - nd.pop("parents") - nd.pop("properties") - # use the remaining diff - propdiff[0].update(od) - propdiff[1].update(nd) - - # As the importance of a property is an attribute of the record - # and not the property, it is not contained in the diff returned - # by compare_entities and needs to be added separately - if (entity0.get_importance(prop) != - entity1.get_importance(matching[0])): - propdiff[0]["importance"] = entity0.get_importance(prop) - propdiff[1]["importance"] = entity1.get_importance(matching[0]) - - # in case there is no difference, we remove the dict keys again - if len(propdiff[0]) == 0 and len(propdiff[1]) == 0: - diff[0]["properties"].pop(prop.name) - diff[1]["properties"].pop(prop.name) + newdiff["properties"][prop.name] = {} + olddiff["properties"][prop.name] = {} + + if (old_entity.get_importance(prop.name) != + new_entity.get_importance(prop.name)): + olddiff["properties"][prop.name]["importance"] = \ + old_entity.get_importance(prop.name) + newdiff["properties"][prop.name]["importance"] = \ + new_entity.get_importance(prop.name) + + if (prop.datatype != matching[0].datatype): + olddiff["properties"][prop.name]["datatype"] = prop.datatype + newdiff["properties"][prop.name]["datatype"] = \ + matching[0].datatype + + if (prop.unit != matching[0].unit): + olddiff["properties"][prop.name]["unit"] = prop.unit + newdiff["properties"][prop.name]["unit"] = \ + matching[0].unit + + if (prop.value != matching[0].value): + # basic comparison of value objects says they are different + same_value = False + if compare_referenced_records: + # scalar reference + if isinstance(prop.value, Entity) and isinstance(matching[0].value, Entity): + # explicitely not recursive to prevent infinite recursion + same_value = empty_diff( + prop.value, matching[0].value, compare_referenced_records=False) + # list of references + elif isinstance(prop.value, list) and isinstance(matching[0].value, list): + # all elements in both lists actually are entity objects + # TODO: check, whether mixed cases can be allowed or should lead to an error + if (all([isinstance(x, Entity) for x in prop.value]) + and all([isinstance(x, Entity) for x in matching[0].value])): + # can't be the same if the lengths are different + if len(prop.value) == len(matching[0].value): + # do a one-by-one comparison: + # the values are the same if all diffs are empty + same_value = all( + [empty_diff(x, y, False) for x, y + in zip(prop.value, matching[0].value)]) + + if not same_value: + olddiff["properties"][prop.name]["value"] = prop.value + newdiff["properties"][prop.name]["value"] = \ + matching[0].value + + if (len(newdiff["properties"][prop.name]) == 0 + and len(olddiff["properties"][prop.name]) == 0): + newdiff["properties"].pop(prop.name) + olddiff["properties"].pop(prop.name) else: raise NotImplementedError( "Comparison not implemented for multi-properties.") - # we have not yet compared properties that do not exist in entity0 - for prop in entity1.properties: - # check how often the property appears in entity0 - num_prop_in_ent0 = len(entity0.properties.filter(prop)) - if num_prop_in_ent0 == 0: - # property is only present in entity0 - add to diff - diff[1]["properties"][prop.name] = {} - if num_prop_in_ent0 > 1: - # Check whether the property is present multiple times in entity0 - # and raise error - result would be incorrect - raise NotImplementedError( - "Comparison not implemented for multi-properties.") + for prop in new_entity.properties: + if len([0 for p in old_entity.properties if p.name == prop.name]) == 0: + newdiff["properties"][prop.name] = {} + + # parents + + for parent in old_entity.parents: + if len([0 for p in new_entity.parents if p.name == parent.name]) == 0: + olddiff["parents"].append(parent.name) - # compare parents - for index, parents, other_entity in [(0, entity0.parents, entity1), - (1, entity1.parents, entity0)]: - for parent in parents: - matching = other_entity.parents.filter(parent) - if len(matching) == 0: - diff[index]["parents"].append(parent.name) - continue + for parent in new_entity.parents: + if len([0 for p in old_entity.parents if p.name == parent.name]) == 0: + newdiff["parents"].append(parent.name) - return diff + return (olddiff, newdiff) def empty_diff(old_entity: Entity, new_entity: Entity, - compare_referenced_records: bool = False, - entity_name_id_equivalency: bool = False) -> bool: + compare_referenced_records: bool = False) -> bool: """Check whether the `compare_entities` found any differences between old_entity and new_entity. @@ -458,13 +351,10 @@ def empty_diff(old_entity: Entity, new_entity: Entity, Whether to compare referenced records in case of both, `old_entity` and `new_entity`, 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) + olddiff, newdiff = compare_entities( + old_entity, new_entity, compare_referenced_records) for diff in [olddiff, newdiff]: for key in ["parents", "properties"]: if len(diff[key]) > 0: @@ -486,9 +376,9 @@ def merge_entities(entity_a: Entity, ) -> Entity: """Merge entity_b into entity_a such that they have the same parents and properties. - The attributes 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 one of those attributes is - set in both entities and they differ, then an + 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, an EntityMergeConflictError will be raised to inform about an unresolvable merge conflict. @@ -496,6 +386,8 @@ def merge_entities(entity_a: Entity, Returns entity_a. + WARNING: This function is currently experimental and insufficiently tested. Use with care. + Parameters ---------- entity_a, entity_b : Entity @@ -528,10 +420,12 @@ def merge_entities(entity_a: Entity, """ + logger.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, - entity_name_id_equivalency=merge_id_with_resolved_entity, - compare_referenced_records=merge_references_with_empty_diffs) + diff_r1, diff_r2 = compare_entities( + entity_a, entity_b, compare_referenced_records=merge_references_with_empty_diffs) # Go through the comparison and try to apply changes to entity_a: for key in diff_r2["parents"]: @@ -551,8 +445,7 @@ def merge_entities(entity_a: Entity, for attribute in ("datatype", "unit", "value"): if (attribute in diff_r2["properties"][key] and diff_r2["properties"][key][attribute] is not None): - if (attribute not in diff_r1["properties"][key] or - diff_r1["properties"][key][attribute] is None): + if (diff_r1["properties"][key][attribute] is None): setattr(entity_a.get_property(key), attribute, diff_r2["properties"][key][attribute]) elif force: @@ -620,18 +513,6 @@ def merge_entities(entity_a: Entity, def describe_diff(olddiff, newdiff, name=None, as_update=True): - """ - This function generates a textual representation of the differences between two entities that have been generated - using 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())) - """ description = "" for attr in list(set(list(olddiff.keys()) + list(newdiff.keys()))): diff --git a/src/linkahead/cached.py b/src/linkahead/cached.py index 11cb959ba10fd507c39eb4d1ddd00bf478859852..f9179e1a54997bf99f7158b9b46e2d1068a21e47 100644 --- a/src/linkahead/cached.py +++ b/src/linkahead/cached.py @@ -116,7 +116,7 @@ def cached_query(query_string: str) -> Container: return result -@lru_cache(maxsize=DEFAULT_SIZE) +@ lru_cache(maxsize=DEFAULT_SIZE) def _cached_access(kind: AccessType, value: Union[str, int], unique: bool = True): # This is the function that is actually cached. # Due to the arguments, the cache has kind of separate sections for cached_query and diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index dd0718c79717bb7a983d8da2b59b9c73ecbd96f3..5689e0799f51584a39c187fd106c4d1b09e9cfc7 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -37,10 +37,8 @@ from __future__ import annotations # Can be removed with 3.10. import re import sys -import warnings from builtins import str from copy import deepcopy -from enum import Enum from datetime import date, datetime from functools import cmp_to_key from hashlib import sha512 @@ -48,6 +46,7 @@ from os import listdir from os.path import isdir from random import randint from tempfile import NamedTemporaryFile + from typing import TYPE_CHECKING from typing import Any, Final, Literal, Optional, TextIO, Union @@ -58,6 +57,7 @@ if TYPE_CHECKING: from os import PathLike QueryDict = dict[str, Optional[str]] + from warnings import warn from lxml import etree @@ -114,8 +114,8 @@ if TYPE_CHECKING: IMPORTANCE = Literal["OBLIGATORY", "RECOMMENDED", "SUGGESTED", "FIX", "NONE"] ROLE = Literal["Entity", "Record", "RecordType", "Property", "File"] -SPECIAL_ATTRIBUTES = ["name", "role", "datatype", "description", "file", - "id", "path", "checksum", "size", "value", "unit"] +SPECIAL_ATTRIBUTES = ["name", "role", "datatype", "description", + "id", "path", "checksum", "size", "value"] class Entity: @@ -138,10 +138,10 @@ class Entity: description: Optional[str] = None, # @ReservedAssignment datatype: Optional[DATATYPE] = None, value=None, - role=None, + **kwargs, ): - self.__role: Optional[ROLE] = role + self.__role: Optional[ROLE] = kwargs["role"] if "role" in kwargs else None self._checksum: Optional[str] = None self._size = None self._upload = None @@ -156,8 +156,8 @@ class Entity: self.datatype: Optional[DATATYPE] = datatype self.value = value self.messages = Messages() - self.properties = PropertyList() - self.parents = ParentList() + self.properties = _Properties() + self.parents = _ParentList() self.path: Optional[str] = None self.file: Optional[File] = None self.unit: Optional[str] = None @@ -922,7 +922,7 @@ class Entity: def get_parents(self): """Get all parents of this entity. - @return: ParentList(list) + @return: _ParentList(list) """ return self.parents @@ -1022,7 +1022,7 @@ class Entity: def get_properties(self): """Get all properties of this entity. - @return: PropertyList(list) + @return: _Properties(list) """ return self.properties @@ -2422,14 +2422,11 @@ class File(Record): value=value, unit=unit, importance=importance, inheritance=inheritance) -class PropertyList(list): - """A list class for Property objects - - This class provides addional functionality like get/set_importance or get_by_name. - """ +class _Properties(list): + """FIXME: Add docstring.""" def __init__(self): - super().__init__() + list.__init__(self) self._importance: dict[Entity, IMPORTANCE] = dict() self._inheritance: dict[Entity, INHERITANCE] = dict() self._element_by_name: dict[str, Entity] = dict() @@ -2522,47 +2519,6 @@ class PropertyList(list): return xml2str(xml) - def filter(self, prop: Optional[Property] = None, pid: Union[None, str, int] = None, - name: Optional[str] = None, check_wrapped: bool = True) -> list: - """ - Return all Properties from the given PropertyList that match the - selection criteria. - - You can provide name or ID and all matching elements will be returned. - If both name and ID are given, elements matching either criterion will - be returned. - - If a Property is given, neither name nor ID may be set. In this case, - only elements matching both name and ID of the Property are returned. - - Also checks the original Properties wrapped within the elements of - PropertyList and will return the original Property if both wrapper and - original match. - - Params - ------ - listobject : Iterable(Property) - List to be filtered - prop : Property - Property to match name and ID with. Cannot be - set simultaneously with ID or name. - pid : str, int - Property ID to match - name : str - Property name to match - check_wrapped : bool, default: True - If set to False, only the wrapper elements - contained in the given PropertyList will be - checked, not the original Properties they wrap. - - Returns - ------- - matches : list - List containing all matching Properties - """ - return _filter_entity_list(self, pid=pid, name=name, entity=prop, - check_wrapped=check_wrapped) - def _get_entity_by_cuid(self, cuid: str): ''' Get the first entity which has the given cuid. @@ -2620,7 +2576,9 @@ class PropertyList(list): raise KeyError(str(prop) + " not found.") -class ParentList(list): +class _ParentList(list): + # TODO unclear why this class is private. Isn't it use full for users? + def _get_entity_by_cuid(self, cuid): ''' Get the first entity which has the given cuid. @@ -2635,8 +2593,8 @@ class ParentList(list): return e raise KeyError("No entity with that cuid in this container.") - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self): + list.__init__(self) self._element_by_name = dict() self._element_by_id = dict() @@ -2649,9 +2607,15 @@ class ParentList(list): if isinstance(parent, list): for p in parent: self.append(p) + return if isinstance(parent, Entity): + if parent.id: + self._element_by_id[str(parent.id)] = parent + + if parent.name: + self._element_by_name[parent.name] = parent list.append(self, parent) else: raise TypeError("Argument was not an Entity") @@ -2693,62 +2657,7 @@ class ParentList(list): return xml2str(xml) - def filter(self, parent: Optional[Parent] = None, pid: Union[None, str, int] = None, - name: Optional[str] = None, check_wrapped: bool = True) -> list: - """ - Return all Parents from the given ParentList that match the selection - criteria. - - You can provide name or ID and all matching elements will be returned. - If both name and ID are given, elements matching either criterion will - be returned. - - If a Parent is given, neither name nor ID may be set. In this case, - only elements matching both name and ID of the Parent are returned. - - Also checks the original Parents wrapped within the elements of - ParentList, will return the original Parent if both wrapper and - original match. - - Params - ------ - listobject : Iterable(Parent) - List to be filtered - parent : Parent - Parent to match name and ID with. Cannot be set - pid : str, int - Parent ID to match - name : str - Parent name to match - simultaneously with ID or name. - check_wrapped : bool, default: True - If set to False, only the wrapper elements - contained in the given ParentList will be - checked, not the original Parents they wrap. - - Returns - ------- - matches : list - List containing all matching Parents - """ - return _filter_entity_list(self, pid=pid, name=name, entity=parent, - check_wrapped=check_wrapped) - def remove(self, parent: Union[Entity, int, str]): - """ - Remove first occurrence of parent. - - Parameters - ---------- - parent: Union[Entity, int, str], the parent to be removed identified via ID or name. If a - Parent object is provided the ID and then the name is used to identify the parent to be - removed. - - Returns - ------- - None - """ - if isinstance(parent, Entity): if parent in self: list.remove(self, parent) @@ -2766,11 +2675,11 @@ class ParentList(list): # by name for e in self: - if e.name is not None and e.name.lower() == parent.name.lower(): + if e.name is not None and e.name == parent.name: list.remove(self, e) return - elif isinstance(parent, str): + elif hasattr(parent, "encode"): # by name for e in self: @@ -2789,19 +2698,6 @@ class ParentList(list): raise KeyError(str(parent) + " not found.") -class _Properties(PropertyList): - def __init__(self, *args, **kwargs): - warnings.warn(DeprecationWarning("This class is deprecated. Please use PropertyList.")) - super().__init__(*args, **kwargs) - - -class _ParentList(ParentList): - def __init__(self, *args, **kwargs): - warnings.warn(DeprecationWarning("This class is deprecated. Please use ParentList " - "(without underscore).")) - super().__init__(*args, **kwargs) - - class Messages(list): """This specialization of list stores error, warning, info, and other messages. The mentioned three messages types play a special role. @@ -5496,100 +5392,3 @@ def delete(ids: Union[list[int], range], raise_exception_on_error: bool = True): c.append(Entity(id=ids)) return c.delete(raise_exception_on_error=raise_exception_on_error) - - -def _filter_entity_list(listobject, entity: Optional[Entity] = None, pid: Union[None, str, int] = None, - name: Optional[str] = None, check_wrapped: bool = True) -> list: - """ - Return all elements from the given list that match the selection criteria. - - You can provide name or ID and all matching elements will be returned. - If both name and ID are given, elements matching either criterion will be - returned. - - If an Entity is given, neither name nor ID may be set. In this case, only - elements matching both name and ID of the Entity are returned, as long as - name and ID are both set. - - In case the elements contained in the given list are wrapped, the function - in its default configuration checks both the wrapped and wrapper Entity - against the match criteria, and will return the wrapped Entity if both - match. Note that this is currently not iterative, meaning that only the - first layer of wrapped entity is considered. - - Params - ------ - listobject : Iterable(Entity) - List to be filtered - entity : Entity - Entity to match name and ID for. Cannot be set - simultaneously with ID or name. - pid : str, int - Entity ID to match - name : str - Entity name to match - check_wrapped : bool, default: True - If set to False, only the wrapper elements - contained in the given list will be checked and - returned, not the original Entities they wrap. - - Returns - ------- - matches : list - A List containing all matching Entities - """ - # Check correct input params and setup - match_entity = False - if entity is not None: - if pid is not None or name is not None: - raise ValueError("Please provide either Entity, pid or name.") - pid = entity.id - name = entity.name - match_entity = True - - # Iterate through list and match based on given criteria - matches = [] - potentials = list(zip(listobject.copy(), [False]*len(listobject))) - for candidate, wrapped_is_checked in potentials: - name_match, pid_match, original_candidate = False, False, None - - # Parents/Properties may be wrapped - if wanted, try to match original - # Note: if we want to check all wrapped Entities, this should be - # switched. First check the wrap, then append wrapped. In this - # case we also don't need wrapped_checked, but preferentially - # append the wrapper. - if check_wrapped and not wrapped_is_checked: - try: - if candidate._wrapped_entity is not None: - original_candidate = candidate - candidate = candidate._wrapped_entity - except AttributeError: - pass - - # Check whether name/pid match - # pid and candidate.id might be int and str, so cast to str (None safe) - if pid is not None and str(candidate.id) == str(pid): - pid_match = True - elif match_entity and pid is None: - # Without a pid we cannot match - pid_match = True - if (name is not None and candidate.name is not None - and candidate.name.lower() == name.lower()): - name_match = True - elif match_entity and name is None: - # Without a name we cannot match - name_match = True - - # If the criteria are satisfied, append the match. Otherwise, check - # the wrapper if applicable - # ToDo: Check whether it would make sense to also check the RecordType - # for equality when match_entity is true to offset potentially - # missing id - if name_match and pid_match: - matches.append(candidate) - elif not match_entity and (name_match or pid_match): - matches.append(candidate) - else: - if original_candidate is not None: - potentials.append((original_candidate, True)) - return matches diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py index 4c529e36a86474500cbaf911df932a89af592d77..4705f19a1bdfbc4358790f787f2dce9ea97fee48 100644 --- a/unittests/test_apiutils.py +++ b/unittests/test_apiutils.py @@ -25,7 +25,6 @@ # Test apiutils # A. Schlemmer, 02/2018 -from io import StringIO import linkahead as db import linkahead.apiutils @@ -97,7 +96,6 @@ def test_resolve_reference(): def test_compare_entities(): - # test compare of parents, properties r1 = db.Record() r2 = db.Record() r1.add_parent("bla") @@ -136,9 +134,13 @@ def test_compare_entities(): assert "tests_234234" in diff_r1["properties"] assert "tests_TT" in diff_r2["properties"] - # test compare units of properties + +def test_compare_entities_units(): r1 = db.Record() r2 = db.Record() + r1.add_parent("bla") + r2.add_parent("bla") + r1.add_parent("lopp") r1.add_property("test", value=2, unit="cm") r2.add_property("test", value=2, unit="m") r1.add_property("tests", value=3, unit="cm") @@ -150,6 +152,8 @@ def test_compare_entities(): diff_r1, diff_r2 = compare_entities(r1, r2) + assert len(diff_r1["parents"]) == 1 + assert len(diff_r2["parents"]) == 0 assert len(diff_r1["properties"]) == 4 assert len(diff_r2["properties"]) == 4 @@ -166,207 +170,14 @@ def test_compare_entities(): assert diff_r2["properties"]["test"]["unit"] == "m" -def test_compare_entities_battery(): - par1, par2, par3 = db.Record(), db.Record(), db.RecordType() - r1, r2, r3 = db.Record(), db.Record(), db.Record() - prop1 = db.Property() - prop2 = db.Property(name="Property 2") - prop3 = db.Property() - - # Basic tests for Properties - prop_settings = {"datatype": db.REFERENCE, "description": "desc of prop", - "value": db.Record().add_parent(par3), "unit": '°'} - t1 = db.Record().add_parent(db.RecordType()) - t2 = db.Record().add_parent(db.RecordType()) - # Change datatype - t1.add_property(db.Property(name="datatype", **prop_settings)) - prop_settings["datatype"] = par3 - t2.add_property(db.Property(name="datatype", **prop_settings)) - # Change description - t1.add_property(db.Property(name="description", **prop_settings)) - prop_settings["description"] = "diff desc" - t2.add_property(db.Property(name="description", **prop_settings)) - # Change value to copy - t1.add_property(db.Property(name="value copy", **prop_settings)) - prop_settings["value"] = db.Record().add_parent(par3) - t2.add_property(db.Property(name="value copy", **prop_settings)) - # Change value to something different - t1.add_property(db.Property(name="value", **prop_settings)) - prop_settings["value"] = db.Record(name="n").add_parent(par3) - t2.add_property(db.Property(name="value", **prop_settings)) - # Change unit - t1.add_property(db.Property(name="unit", **prop_settings)) - prop_settings["unit"] = db.Property(unit='°') - t2.add_property(db.Property(name="unit", **prop_settings)) - # Change unit again - t1.add_property(db.Property(name="unit 2", **prop_settings)) - prop_settings["unit"] = db.Property() - t2.add_property(db.Property(name="unit 2", **prop_settings)) - # Compare - diff_0 = compare_entities(t1, t2) - diff_1 = compare_entities(t1, t2, compare_referenced_records=True) - # Check correct detection of changes - assert diff_0[0]["properties"]["datatype"] == {"datatype": db.REFERENCE} - assert diff_0[1]["properties"]["datatype"] == {"datatype": par3} - assert diff_0[0]["properties"]["description"] == {"description": "desc of prop"} - assert diff_0[1]["properties"]["description"] == {"description": "diff desc"} - assert "value" in diff_0[0]["properties"]["value copy"] - assert "value" in diff_0[1]["properties"]["value copy"] - assert "value" in diff_0[0]["properties"]["value"] - assert "value" in diff_0[1]["properties"]["value"] - assert "unit" in diff_0[0]["properties"]["unit"] - assert "unit" in diff_0[1]["properties"]["unit"] - assert "unit" in diff_0[0]["properties"]["unit 2"] - assert "unit" in diff_0[1]["properties"]["unit 2"] - # Check correct result for compare_referenced_records=True - assert "value copy" not in diff_1[0]["properties"] - assert "value copy" not in diff_1[1]["properties"] - diff_0[0]["properties"].pop("value copy") - diff_0[1]["properties"].pop("value copy") - assert diff_0 == diff_1 - - # Basic tests for Parents - t3 = db.Record().add_parent(db.RecordType("A")).add_parent(db.Record("B")) - t4 = db.Record().add_parent(db.RecordType("A")) - assert compare_entities(t3, t4)[0]['parents'] == ['B'] - assert len(compare_entities(t3, t4)[1]['parents']) == 0 - t4.add_parent(db.Record("B")) - assert empty_diff(t3, t4) - # The two following assertions document current behaviour but do not make a - # lot of sense - t4.add_parent(db.Record("B")) - assert empty_diff(t3, t4) - t3.add_parent(db.RecordType("A")).add_parent(db.Record("B")) - t4.add_parent(db.RecordType("B")).add_parent(db.Record("A")) - assert empty_diff(t3, t4) - - # Basic tests for special attributes - prop_settings = {"id": 42, "name": "Property", - "datatype": db.LIST(db.REFERENCE), "value": [db.Record()], - "unit": '€', "description": "desc of prop"} - alt_settings = {"id": 64, "name": "Property 2", - "datatype": db.LIST(db.TEXT), "value": [db.RecordType()], - "unit": '€€', "description": " ę Ě ப ཾ ཿ ∛ ∜ ㅿ ㆀ 값 "} - t5 = db.Property(**prop_settings) - t6 = db.Property(**prop_settings) - assert empty_diff(t5, t6) - # ID - t5.id = alt_settings['id'] - diff = compare_entities(t5, t6) - assert diff[0] == {'properties': {}, 'parents': [], 'id': alt_settings['id']} - assert diff[1] == {'properties': {}, 'parents': [], 'id': prop_settings['id']} - t6.id = alt_settings['id'] - assert empty_diff(t5, t6) - # Name - t5.name = alt_settings['name'] - diff = compare_entities(t5, t6) - assert diff[0] == {'properties': {}, 'parents': [], 'name': alt_settings['name']} - assert diff[1] == {'properties': {}, 'parents': [], 'name': prop_settings['name']} - t6.name = alt_settings['name'] - assert empty_diff(t5, t6) - # Description - t6.description = alt_settings['description'] - diff = compare_entities(t5, t6) - assert diff[0] == {'properties': {}, 'parents': [], 'description': prop_settings['description']} - assert diff[1] == {'properties': {}, 'parents': [], 'description': alt_settings['description']} - t5.description = alt_settings['description'] - assert empty_diff(t5, t6) - # Unit - t5.unit = alt_settings['unit'] - diff = compare_entities(t5, t6) - assert diff[0] == {'properties': {}, 'parents': [], 'unit': alt_settings['unit']} - assert diff[1] == {'properties': {}, 'parents': [], 'unit': prop_settings['unit']} - t6.unit = alt_settings['unit'] - assert empty_diff(t5, t6) - # Value - t6.value = alt_settings['value'] - diff = compare_entities(t5, t6) - assert diff[0] == {'properties': {}, 'parents': [], 'value': prop_settings['value']} - assert diff[1] == {'properties': {}, 'parents': [], 'value': alt_settings['value']} - t5.value = alt_settings['value'] - assert empty_diff(t5, t6) - # Datatype - t6.datatype = alt_settings['datatype'] - diff = compare_entities(t5, t6) - assert diff[0] == {'properties': {}, 'parents': [], 'datatype': prop_settings['datatype']} - assert diff[1] == {'properties': {}, 'parents': [], 'datatype': alt_settings['datatype']} - t5.datatype = alt_settings['datatype'] - assert empty_diff(t5, t6) - # All at once - diff = compare_entities(db.Property(**prop_settings), db.Property(**alt_settings)) - assert diff[0] == {'properties': {}, 'parents': [], **prop_settings} - assert diff[1] == {'properties': {}, 'parents': [], **alt_settings} - # Entity Type - diff = compare_entities(db.Property(value=db.Property(id=101)), - db.Property(value=db.Record(id=101))) - assert "value" in diff[0] - assert "value" in diff[1] - diff = compare_entities(db.Property(value=db.Record(id=101)), - db.Property(value=db.Record(id=101))) - assert "value" in diff[0] - assert "value" in diff[1] - assert empty_diff(db.Property(value=db.Record(id=101)), - db.Property(value=db.Record(id=101)), - compare_referenced_records=True) - - # Special cases - # Files - assert not empty_diff(db.File(path='ABC', file=StringIO("ABC")), - db.File(path='ABC', file=StringIO("Other"))) - # Importance - assert empty_diff(db.Property().add_property(prop1), - db.Property().add_property(prop1)) - assert not empty_diff(db.Property().add_property(prop1, importance=db.SUGGESTED), - db.Property().add_property(prop1, importance=db.OBLIGATORY)) - # Mixed Lists - assert empty_diff(db.Property(value=[1, 2, 'a', r1]), - db.Property(value=[1, 2, 'a', r1])) - # entity_name_id_equivalency - assert not empty_diff(db.Property(value=[1, db.Record(id=2), 3, db.Record(id=4)]), - db.Property(value=[db.Record(id=1), 2, db.Record(id=3), 4])) - assert empty_diff(db.Property(value=[1, db.Record(id=2), 3, db.Record(id=4)]), - db.Property(value=[db.Record(id=1), 2, db.Record(id=3), 4]), - entity_name_id_equivalency=True) - assert empty_diff(db.Property(value=1), db.Property(value=db.Record(id=1)), - entity_name_id_equivalency=True) - # entity_name_id_equivalency - prop4 = db.Property(**prop_settings).add_parent(par1).add_property(prop2) - prop4_c = db.Property(**prop_settings).add_parent(par1).add_property(prop2) - prop4.value = db.Record(id=12) - prop4_c.value = '12' - prop4.add_property(db.Property(name="diff", datatype=db.LIST(db.REFERENCE), - value=[12, db.Record(id=13), par1, "abc%"])) - prop4_c.add_property(db.Property(name="diff", datatype=db.LIST(db.REFERENCE), - value=[db.Record(id=12), "13", par1, "abc%"])) - assert not empty_diff(prop4, prop4_c, entity_name_id_equivalency=False) - assert empty_diff(prop4, prop4_c, entity_name_id_equivalency=True) - # Order invariance - t7 = db.Property(**prop_settings).add_parent(par1).add_property(prop1) - t8 = db.Property(**alt_settings).add_parent(par3).add_property(prop3) - diffs_0 = compare_entities(t7, t8), compare_entities(t7, t8, True) - diffs_1 = compare_entities(t8, t7)[::-1], compare_entities(t8, t7, True)[::-1] - assert diffs_0 == diffs_1 - prop_settings = {"datatype": db.REFERENCE, "description": "desc of prop", - "value": db.Record().add_parent(par3), "unit": '°'} - t1.add_property(db.Property(name="description", **prop_settings)) - t2.add_property(db.Property(name="description", **prop_settings)) - try: - diffs_0 = compare_entities(t1, t2), compare_entities(t1, t2, True) - except Exception as e: - diffs_0 = type(e) - try: - diffs_1 = compare_entities(t2, t1)[::-1], compare_entities(t2, t1, True)[::-1] - except Exception as e: - diffs_1 = type(e) - assert diffs_0 == diffs_1 - - def test_compare_special_properties(): # Test for all known special properties: + SPECIAL_PROPERTIES = ("description", "name", + "checksum", "size", "path", "id") INTS = ("size", "id") HIDDEN = ("checksum", "size") - for key in SPECIAL_ATTRIBUTES: + for key in SPECIAL_PROPERTIES: set_key = key if key in HIDDEN: set_key = "_" + key @@ -404,7 +215,8 @@ def test_compare_special_properties(): assert len(diff_r1["properties"]) == 0 assert len(diff_r2["properties"]) == 0 - # compare Property objects + +def test_compare_properties(): p1 = db.Property() p2 = db.Property() @@ -655,10 +467,10 @@ def test_empty_diff(): rec_a.remove_property("RefType") rec_b.remove_property("RefType") assert empty_diff(rec_a, rec_b) - rec_a.add_property(name="RefType", datatype=db.LIST("RefType"), - value=[ref_rec_a, ref_rec_a]) - rec_b.add_property(name="RefType", datatype=db.LIST("RefType"), - value=[ref_rec_b, ref_rec_b]) + rec_a.add_property(name="RefType", datatype=db.LIST( + "RefType"), value=[ref_rec_a, ref_rec_a]) + rec_b.add_property(name="RefType", datatype=db.LIST( + "RefType"), value=[ref_rec_b, ref_rec_b]) assert not empty_diff(rec_a, rec_b) assert empty_diff(rec_a, rec_b, compare_referenced_records=True) @@ -789,12 +601,13 @@ def test_merge_id_with_resolved_entity(): # Overwrite from right to left in both cases merge_entities(recA, recB, merge_id_with_resolved_entity=True) - assert recA.get_property(rtname).value == ref_rec + assert recA.get_property(rtname).value == ref_id + assert recA.get_property(rtname).value == recB.get_property(rtname).value recA = db.Record().add_property(name=rtname, value=ref_rec) merge_entities(recB, recA, merge_id_with_resolved_entity=True) - assert recB.get_property(rtname).value == ref_id - assert recA.get_property(rtname).value == ref_rec + assert recB.get_property(rtname).value == ref_rec + assert recA.get_property(rtname).value == recB.get_property(rtname).value # id mismatches recB = db.Record().add_property(name=rtname, value=ref_id*2) @@ -810,8 +623,7 @@ def test_merge_id_with_resolved_entity(): # also works in lists: recA = db.Record().add_property( name=rtname, datatype=db.LIST(rtname), value=[ref_rec, ref_id*2]) - recB = db.Record().add_property( - name=rtname, datatype=db.LIST(rtname), value=[ref_id, ref_id*2]) + recB = db.Record().add_property(name=rtname, datatype=db.LIST(rtname), value=[ref_id, ref_id*2]) 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] + assert recA.get_property(rtname).value == [ref_id, ref_id*2] + assert recA.get_property(rtname).value == recB.get_property(rtname).value diff --git a/unittests/test_entity.py b/unittests/test_entity.py index 66cffbefe207820ddde1463f62a88788beb7df9a..abf82f0a9b557cf9d1d2365e01fedaa4eae0c565 100644 --- a/unittests/test_entity.py +++ b/unittests/test_entity.py @@ -22,17 +22,14 @@ # ** end header # """Tests for the Entity class.""" -import os # pylint: disable=missing-docstring import unittest -from pytest import raises +from lxml import etree -import linkahead -from linkahead import (INTEGER, Entity, Property, Record, RecordType, Parent, +import os +from linkahead import (INTEGER, Entity, Property, Record, RecordType, configure_connection) -from linkahead.common.models import SPECIAL_ATTRIBUTES from linkahead.connection.mockup import MockUpServerConnection -from lxml import etree UNITTESTDIR = os.path.dirname(os.path.abspath(__file__)) @@ -85,13 +82,7 @@ class TestEntity(unittest.TestCase): self.assertEqual(entity.to_xml().tag, "Property") def test_instantiation(self): - e = Entity() - for attr in SPECIAL_ATTRIBUTES: - assert hasattr(e, attr) - - def test_instantiation_bad_argument(self): - with self.assertRaises(Exception): - Entity(rol="File") + self.assertRaises(Exception, Entity()) def test_parse_role(self): """During parsing, the role of an entity is set explicitely. All other @@ -106,135 +97,3 @@ class TestEntity(unittest.TestCase): # test whether the __role property of this object has explicitely been # set. self.assertEqual(getattr(entity, "_Entity__role"), "Record") - - -def test_parent_list(): - p1 = RecordType(name="A") - pl = linkahead.common.models.ParentList([p1]) - assert p1 in pl - assert pl.index(p1) == 0 - assert RecordType(name="A") not in pl - assert RecordType(id=101) not in pl - p2 = RecordType(id=101) - pl.append(p2) - assert p2 in pl - assert len(pl) == 2 - p3 = RecordType(id=103, name='B') - pl.append(p3) - assert len(pl) == 3 - - # test removal - # remove by id only, even though element in parent list has name and id - pl.remove(RecordType(id=103)) - assert len(pl) == 2 - assert p3 not in pl - assert p2 in pl - assert p1 in pl - # Same for removal by name - pl.append(p3) - assert len(pl) == 3 - pl.remove(RecordType(name='B')) - assert len(pl) == 2 - assert p3 not in pl - # And an error if no suitable element can be found - with raises(KeyError) as ve: - pl.remove(RecordType(id=105, name='B')) - assert "not found" in str(ve.value) - assert len(pl) == 2 - - # TODO also check pl1 == pl2 - - -def test_property_list(): - # TODO: Resolve parent-list TODOs, then transfer to here. - # TODO: What other considerations have to be done with properties? - p1 = Property(name="A") - pl = linkahead.common.models.PropertyList() - pl.append(p1) - assert p1 in pl - assert Property(id=101) not in pl - p2 = Property(id=101) - pl.append(p2) - assert p1 in pl - assert p2 in pl - p3 = Property(id=103, name='B') - pl.append(p3) - - -def test_filter(): - rt1 = RecordType(id=100) - rt2 = RecordType(id=101, name="RT") - rt3 = RecordType() - p1 = Property(id=100) - p2 = Property(id=100) - p3 = Property(id=101, name="RT") - p4 = Property(id=102, name="P") - p5 = Property(id=103, name="P") - p6 = Property() - r1 = Record(id=100) - r2 = Record(id=100) - r3 = Record(id=101, name="RT") - r4 = Record(id=101, name="R") - r5 = Record(id=104, name="R") - r6 = Record(id=105, name="R") - test_ents = [rt1, rt2, rt3, p1, p2, p3, p4, p5, p6, r1, r2, r3, r4, r5, r6] - - # Setup - t1 = Property() - t1_props, t1_pars = t1.properties, t1.parents - t2 = Record() - t2_props, t2_pars = t2.properties, t2.parents - t3 = RecordType() - t3_props, t3_pars = t3.properties, t3.parents - test_colls = [t1_props, t1_pars, t2_props, t2_pars, t3_props, t3_pars] - for coll in test_colls: - for ent in test_ents: - assert ent not in coll - assert ent not in coll.filter(ent) - - # Checks with each type - for t, t_props, t_pars in [(t1, t1_props, t1_pars), (t2, t2_props, t2_pars), - (t3, t3_props, t3_pars)]: - # Properties - # Basic Checks - t.add_property(p1) - t.add_property(p3) - assert p1 in t_props.filter(pid=100) - assert p1 in t_props.filter(pid="100") - assert p1 not in t_props.filter(pid=101, name="RT") - for entity in [rt1, p2, r1, r2]: - assert entity not in t_props.filter(pid=100) - assert p1 in t_props.filter(entity) - # Check that direct addition (not wrapped) works - t_props.append(p2) - assert p2 in t_props.filter(pid=100) - assert p2 not in t_props.filter(pid=101, name="RT") - for entity in [rt1, r1, r2]: - assert entity not in t_props.filter(pid=100) - assert p2 in t_props.filter(entity) - - # Parents - # Filtering with both name and id - t.add_parent(r3) - t.add_parent(r5) - assert r3 in t_pars.filter(pid=101) - assert r5 not in t_pars.filter(pid=101) - assert r3 not in t_pars.filter(name="R") - assert r5 in t_pars.filter(name="R") - assert r3 in t_pars.filter(pid=101, name="R") - assert r5 in t_pars.filter(pid=101, name="R") - assert r3 in t_pars.filter(pid=104, name="RT") - assert r5 in t_pars.filter(pid=104, name="RT") - assert r3 not in t_pars.filter(pid=105, name="T") - assert r5 not in t_pars.filter(pid=105, name="T") - # Works also without id / name and with duplicate parents - for ent in test_ents: - t.add_parent(ent) - for ent in test_ents: - assert ent in t_pars.filter(ent) - for ent in [rt1, p1, p2, r1, r2]: - filtered = t_pars.filter(ent) - for ent2 in [rt1, p1, p2, r1, r2]: - assert ent2 in filtered - assert ent in t_pars.filter(pid=100) - assert ent in t_pars.filter(pid="100")