diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py index e4608622f44178e3f13645ca5150cb6e15d04fab..d61daeecb3d1ac2305090e7b7210d9c2c40bd33f 100644 --- a/src/linkahead/apiutils.py +++ b/src/linkahead/apiutils.py @@ -264,12 +264,12 @@ def compare_entities(entity0: Optional[Entity] = None, 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") + 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") + raise ValueError("You cannot use both entity1 and new_entity") entity1 = new_entity diff: tuple = ({"properties": {}, "parents": []}, @@ -371,7 +371,7 @@ def compare_entities(entity0: Optional[Entity] = None, # compare properties for prop in entity0.properties: - matching = entity1.properties.filter(name=prop.name, pid=prop.id, check_wrapped=False) + matching = entity1.properties.filter(name=prop.name, pid=prop.id) if len(matching) == 0: # entity1 has prop, entity0 does not diff[0]["properties"][prop.name] = {} diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index 347d413808d680fc17a0fd2ccdb5c22ae39b1701..1dbeb802311c7afaea2340af15e49537520ef57f 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -2503,38 +2503,31 @@ 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: + def filter(self, prop: Optional[Property] = None, + pid: Union[None, str, int] = None, + name: Optional[str] = None, + conjunction: bool = False) -> 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. + Please refer to the documentation of _filter_entity_list for a detailed + description of behaviour. 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. + 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. + conjunction : bool, defaults to False + Set to return only entities that match both id and name + if both are given. Returns ------- @@ -2542,7 +2535,7 @@ class PropertyList(list): List containing all matching Properties """ return _filter_entity_list(self, pid=pid, name=name, entity=prop, - check_wrapped=check_wrapped) + conjunction=conjunction) def _get_entity_by_cuid(self, cuid: str): ''' @@ -2674,22 +2667,16 @@ 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: + def filter(self, parent: Optional[Parent] = None, + pid: Union[None, str, int] = None, + name: Optional[str] = None, + conjunction: bool = False) -> 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. + Please refer to the documentation of _filter_entity_list for a detailed + description of behaviour. Params ------ @@ -2702,10 +2689,9 @@ class ParentList(list): 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. + conjunction : bool, defaults to False + Set to return only entities that match both id and name + if both are given. Returns ------- @@ -2713,7 +2699,7 @@ class ParentList(list): List containing all matching Parents """ return _filter_entity_list(self, pid=pid, name=name, entity=parent, - check_wrapped=check_wrapped) + conjunction=conjunction) def remove(self, parent: Union[Entity, int, str]): """ @@ -5479,25 +5465,33 @@ 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: list[Entity], entity: Optional[Entity] = None, pid: Union[None, str, int] = None, - name: Optional[str] = None, conjunction: bool = False) -> list: +def _filter_entity_list(listobject: list[Entity], + entity: Optional[Entity] = None, + pid: Union[None, str, int] = None, + name: Optional[str] = None, + conjunction: bool = False) -> list: """ - Returns a subset of the entities in the list based on whether their ID and name matches the - selection criterion. + Returns a subset of entities from the list based on whether their id and + name matches the selection criterion. - If an entity in the list has - - ID and name are None: will never be returned - - ID is not None and name is None: will be returned if the ID matches a given not-None value - - ID is None and name is not None: will be returned if the name matches a given not-None value - - ID and name are not None: will be returned if the ID matches a given not-None value, if no ID - was given, the entity will be returned if the name matches. + If both pid and name are given, entities from the list are first matched + based on id. If they do not have an id, they are matched based on name. + If only one parameter is given, only this parameter is considered. - As IDs can be strings, integer IDs will be casted to string for the comparison. + If an Entity is given, neither name nor ID may be set. In this case, pid + and name are determined by the attributes of given entity. - If an Entity is given, neither name nor ID may be set. In this case, ID and name are taken from - the given entity, but the behavior is the same. + This results in the following selection criteria: + If an entity in the list + - has both name and id, it is returned if the id matches the given not-None + value for pid. If no pid was given, it is returned if the name matches. + - has an id, but no name, it will be returned only if it matches the given + not-None value + - has no id, but a name, it will be returned if the name matches the given + not-None value + - has neither id nor name, it will never be returned - TODO explain conjunction -> Not implementd + As IDs can be strings, integer IDs are cast to string for the comparison. Params ------ @@ -5510,20 +5504,28 @@ def _filter_entity_list(listobject: list[Entity], entity: Optional[Entity] = Non Entity ID to match name : str Entity name to match + conjunction : bool, defaults to False + Set to true to return only entities that match both id + and name if both are given. Returns ------- matches : list A List containing all matching Entities """ - if conjunction: - raise NotImplementedError("conjunction is not yet implemented") # Check correct input params and setup if entity is not None: if pid is not None or name is not None: - raise ValueError("Please provide either Entity, pid or name.") + raise ValueError("If an entity is given, pid and name must not be set.") pid = entity.id name = entity.name + if pid is None and name is None: + if entity is None: + raise ValueError("One of entity, pid or name must be set.") + else: + raise ValueError("A given entity must have at least one of name and id.") + if pid is None or name is None: + conjunction = False # Iterate through list and match based on given criteria matches = [] @@ -5531,23 +5533,21 @@ def _filter_entity_list(listobject: list[Entity], entity: Optional[Entity] = Non name_match, pid_match = False, False # 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 + # Comparison is only possible if both are not None + pid_none = pid is None or candidate.id is None + # Cast to string in case one is f.e. "12" and the other is 12 + if not pid_none and str(candidate.id) == str(pid): 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_none = name is None or candidate.name is None + if not name_none and str(candidate.name).lower() == str(name).lower(): name_match = True - # If the criteria are satisfied, append the match. Otherwise, check - # the wrapper if applicable - if name_match and pid_match: - matches.append(candidate) - elif not match_entity and (name_match or pid_match): + # If the criteria are satisfied, append the match. + if pid_match and name_match: matches.append(candidate) + elif not conjunction: + if pid_match: + matches.append(candidate) + if pid_none and name_match: + matches.append(candidate) return matches diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py index 4d59950733ca7070d2a9c5cb4973ef8a167d49ea..35f80e5e3dbe355b9bc32330587f6aa7bc2b0d63 100644 --- a/unittests/test_apiutils.py +++ b/unittests/test_apiutils.py @@ -261,16 +261,16 @@ def test_compare_entities_units(): def test_compare_entities_battery(): - par1, par2, par3 = db.Record(), db.Record(), db.RecordType() + par1, par3 = db.Record(name=""), db.RecordType(name="") r1, r2, r3 = db.Record(), db.Record(), db.Record() prop2 = db.Property(name="Property 2") - prop3 = db.Property() + prop3 = db.Property(name="") # 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()) + t1 = db.Record().add_parent(db.RecordType(id=1)) + t2 = db.Record().add_parent(db.RecordType(id=1)) # Change datatype t1.add_property(db.Property(name="datatype", **prop_settings)) prop_settings["datatype"] = par3 @@ -335,10 +335,10 @@ def test_compare_entities_battery(): # Basic tests for special attributes prop_settings = {"id": 42, "name": "Property", - "datatype": db.LIST(db.REFERENCE), "value": [db.Record()], + "datatype": db.LIST(db.REFERENCE), "value": [db.Record(name="")], "unit": '€', "description": "desc of prop"} alt_settings = {"id": 64, "name": "Property 2", - "datatype": db.LIST(db.TEXT), "value": [db.RecordType()], + "datatype": db.LIST(db.TEXT), "value": [db.RecordType(name="")], "unit": '€€', "description": " ę Ě ப ཾ ཿ ∛ ∜ ㅿ ㆀ 값 "} t5 = db.Property(**prop_settings) t6 = db.Property(**prop_settings) diff --git a/unittests/test_entity.py b/unittests/test_entity.py index 5c6752c3e5ca52cf616606ea3f235e009542600b..3fb014f864b5e79844be7165e3b0093bca731451 100644 --- a/unittests/test_entity.py +++ b/unittests/test_entity.py @@ -164,14 +164,13 @@ def test_property_list(): def test_filter(): rt1 = RecordType(id=100) rt2 = RecordType(id=101, name="RT") - rt3 = RecordType() - # TODO add name only + rt3 = RecordType(name="") p1 = Property(id=100) - p2 = Property(id=100) # TODO remove + p2 = Property(id=100) p3 = Property(id=101, name="RT") p4 = Property(id=102, name="P") p5 = Property(id=103, name="P") - p6 = Property() + p6 = Property(name="") r1 = Record(id=100) r2 = Record(id=100) r3 = Record(id=101, name="RT") @@ -181,7 +180,7 @@ def test_filter(): test_ents = [rt1, rt2, rt3, p1, p2, p3, p4, p5, p6, r1, r2, r3, r4, r5, r6] # Setup - for entity in [Property(), Record(), RecordType()]: + for entity in [Property(name=""), Record(name=""), RecordType(name="")]: for coll in [entity.properties, entity.parents]: for ent in test_ents: assert ent not in coll @@ -224,8 +223,8 @@ def test_filter(): assert tr3 not in t_pars.filter(name="R") assert tr5 in t_pars.filter(name="R") assert tr3 in t_pars.filter(pid=101, name="R") - assert tr5 in t_pars.filter(pid=101, name="R") - assert tr3 in t_pars.filter(pid=104, name="RT") + assert tr5 not in t_pars.filter(pid=101, name="R") + assert tr3 not in t_pars.filter(pid=104, name="RT") assert tr5 in t_pars.filter(pid=104, name="RT") assert tr3 not in t_pars.filter(pid=105, name="T") assert tr5 not in t_pars.filter(pid=105, name="T")