diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index 0df706bd6e8bd1be7c7504d9de2590614f9fef27..fe15b1a7f843af110471a624802b3a93efe8a035 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -2522,23 +2522,50 @@ class PropertyList(list): return xml2str(xml) - def filter(self, pid:Union[str, int]=None, name:str =None, prop:Property=None): + def filter(self, prop:Property = None, pid:Union[str, int] = None, + name:str = None, check_equality: bool = False, + check_wrap: bool = True) -> list: """ - Filters all Properties from this PropertyList that match either name or ID. + Return all Properties from the given PropertyList that match the + selection criteria. - You can provide name and or ID via the corresponding arguments or you - pass a Property object to this function. + 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_equality : bool, default: False + If set to True, potential matches will be checked + using the equality operator instead of ID and name. + Will be ignored if the prop parameter is not set. + check_wrap : 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. - Parameters - ---------- - pid: Union[str,int], ID of the Properties to be returned - name: str, name of the Properties to be returned - prop: Property, name of the Properties to be returned Returns ------- - list, a list with all matching Properties + list with all matching Properties """ - return _filter_entity_list(self, "Property", pid=pid, name=name, element=prop) + return _filter_entity_list(self, pid=pid, name=name, entity=prop, + check_equality=check_equality, check_wrap=check_wrap) def _get_entity_by_cuid(self, cuid: str): ''' @@ -2670,18 +2697,50 @@ class ParentList(list): return xml2str(xml) - def filter(self, pid:Union[str, int]=None, name:str =None, parent:Parent=None): + def filter(self, parent:Parent = None, pid:Union[str, int] = None, + name:str = None, check_equality: bool = False, + check_wrap: bool = True) -> list: """ - Filters all Parents from this ParentList that match either name or ID. + Return all Parents from the given ParentList that match the selection + criteria. - You can provide name and or ID via the corresponding arguments or you - pass a Parent object to this function. + 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_equality : bool, default: False + If set to True, potential matches will be checked + using the equality operator instead of ID and name. + Will be ignored if the entity parameter is not set. + check_wrap : 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 ------- - a list with all matching Parents + list with all matching Parents """ - return _filter_entity_list(self, "Parent", pid=pid, name=name, element=parent) + return _filter_entity_list(self, pid=pid, name=name, entity=parent, + check_equality=check_equality, check_wrap=check_wrap) def remove(self, parent: Union[Entity, int, str]): """ @@ -5446,29 +5505,108 @@ def delete(ids: Union[list[int], range], raise_exception_on_error: bool = True): return c.delete(raise_exception_on_error=raise_exception_on_error) -def _filter_entity_list(listobject, element_type, pid:Union[str, int]=None, name:str =None, element:Any=None): - """ - Filterss all elements from the given list that match either name or ID. - You can provide name and or ID via the corresponding arguments or you - pass an object to this function that has id and/or name. +def _filter_entity_list(listobject, entity:Entity = None, pid:Union[str, int] = None, + name:str = None, check_equality:bool = False, + check_wrap: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. + + 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_equality : bool, default: False + If set to True, potential matches will be checked + using the equality operator instead of ID and name. + Will be ignored if the entity parameter is not set. + check_wrap : bool, default: True + If set to False, only the wrapper elements + contained in the given list will be checked, not + the original Entities they wrap. + + Returns + ------- + list with 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 + else: + check_equality = False + + # 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 indicated, 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_wrap and not wrapped_is_checked: + try: + if candidate._wrapped_entity is not None: + original_candidate = candidate + candidate = candidate._wrapped_entity + except: + pass - Returns - ------- - a list with all matching elements - """ - if element is not None: - if pid is not None or name is not None: - raise ValueError(f"Please provide either a {element_type} or one of " - "pid or name") - pid = element.id - name = element.name - - candidates = [] - if name is not None: - candidates.extend( - [p for p in listobject if p.name is not None and p.name.lower() == name.lower()]) - if pid is not None: - candidates.extend( - [p for p in listobject if p.id == pid]) - return candidates + # If indicated, only consider equality + if check_equality: + if candidate == entity: + matches.append(candidate) + elif original_candidate is not None: + potentials.append((original_candidate, True)) + continue + # Otherwise, check whether name/pid match + if pid is not None and candidate.id == pid: + pid_match = True + elif match_entity and candidate.id == pid: + # If we are matching the entity, both being Null is also satisfactory + 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 candidate.name == name: + # If we are matching the entity, both being Null is also satisfactory + 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 RecordType + # for match_entity + 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_entity.py b/unittests/test_entity.py index 1b3e9d24e3eb8c79d05bd372e18ea27e3f55ddb6..cff6d8dc01a04688232f92ba5e9064755b5bff1b 100644 --- a/unittests/test_entity.py +++ b/unittests/test_entity.py @@ -119,17 +119,9 @@ def test_parent_list(): pl.append(p2) assert p2 in pl assert len(pl) == 2 - assert p1 in pl.filter(name="A") - assert p2 in pl.filter(pid=101) - assert p2 in pl.filter(pid=101, name="A") - assert p1 in pl.filter(pid=101, name="A") - assert p1 in pl.filter(parent=Parent(id=101, name="A")) - assert p2 in pl.filter(parent=Parent(id=101, name="A")) p3 = RecordType(id=103, name='B') pl.append(p3) assert len(pl) == 3 - assert p3 in pl.filter(name="B") - assert p3 in pl.filter(pid=103) # test removal # remove by id only, even though element in parent list has name and id @@ -153,8 +145,6 @@ def test_parent_list(): # 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? @@ -170,11 +160,81 @@ def test_property_list(): p3 = Property(id=103, name='B') pl.append(p3) - assert p1 in pl.filter(name="A") - assert p2 in pl.filter(pid=101) - assert p2 in pl.filter(pid=101, name="A") - assert p1 in pl.filter(pid=101, name="A") - assert p1 in pl.filter(prop=Property(id=101, name="A")) - assert p2 in pl.filter(prop=Property(id=101, name="A")) - assert p3 in pl.filter(name="B") - assert p3 in pl.filter(pid=103) + +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(p1, check_equality=True) + 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) + assert p1 not in t_props.filter(entity, check_equality=True) + # Check that direct addition (not wrapped) works + t_props.append(p2) + assert p2 in t_props.filter(pid=100) + assert p2 in t_props.filter(p2, check_equality=True) + 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) + assert p2 not in t_props.filter(entity, check_equality=True) + + ## 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) + assert ent in t_pars.filter(ent, check_equality=True) + # ToDo: Check whether duplicates are wanted and write tests for the + # desirable outcome