diff --git a/src/doc/tutorials/complex_data_models.rst b/src/doc/tutorials/complex_data_models.rst index b33bc9ae49e9a850a73e32e396735652ecaab7d7..168cf3b9f0d6839ed8f78beb01ae24fb9d489e88 100644 --- a/src/doc/tutorials/complex_data_models.rst +++ b/src/doc/tutorials/complex_data_models.rst @@ -51,18 +51,18 @@ Examples # Very complex part of the data model: # Case 1: File added to another file f2.add_property(p1, value=f1) # this adds a file property with value first file - # to the second file + # to the second file # Case 2: Property added to a property p2.add_property(p3, value=27) # this adds an integer property with value 27 to the - # double property + # double property # Case 3: Reference property added to a property # The property p2 now has two sub properties, one is pointing to # record p2 which itself has the property p2, therefore this can be # considered a loop in the data model. p2.add_property(p4, value=r2) # this adds a reference property pointing to - # record 2 to the double property + # record 2 to the double property # Insert a container containing all the newly created entities: c = db.Container().extend([rt1, rt2, r1, r2, f1, p1, p2, p3, f2, p4]) @@ -87,46 +87,42 @@ entities. A short example: import linkahead as db - # Setup a record with four properties + # Setup a record with six 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) + p1_1 = db.Property(id=101, name="Property 1") + p1_2 = db.Property(name="Property 1") + p2_1 = db.Property(id=102, name="Property 2") + p2_2 = db.Property(id=102) + p2_3 = db.Property(id=102, name="Other Property") + p3 = db.Property(id=104, name="Other Property") + r.add_property(p1_1).add_property(p1_2).add_property(p2_1) + r.add_property(p2_2).add_property(p2_3).add_property(p3) properties = r.properties - # As r only has one property with id 101, this returns a list containing only p1 + # As r only has one property with id 101, this returns a list containing only p1_1 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 + # Result: [p1_1] + + # Filtering with name="Property 1" returns both p1_1 and p1_2, as they share their name + properties.filter(name="Property 1") + # Result: [p1_1, p1_2] + + # If both name and pid are given, matching is based only on pid for all entities that have an id + properties.filter(pid="102", name="Other Property") + # Result: [p2_1, p2_2, p2_3] + + # However, filtering with name="Property 1" and id=101 returns both p1_1 and p1_2, because + # p1_2 does not have an id and matches the name + properties.filter(pid="101", name="Property 1") + # Result: [p1_1, p1_2] + + # We can also filter using an entity, in which case the name and id of the entity are used: + properties.filter(pid="102", name="Property 2") == properties.filter(p2_1) + # Result: True + + # If we only need properties that match both id and name, we can set the parameter + # conjunction to True: + properties.filter(pid="102", name="Property 2", conjunction=True) + # Result: [p2_1] The filter function of ParentList works analogously. diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py index d51171c7c59fd0ae8ee202db224f2597f3e9cdae..17bd5af4b223b9d0db84b2124b0393e07ba2f80c 100644 --- a/src/linkahead/apiutils.py +++ b/src/linkahead/apiutils.py @@ -251,12 +251,16 @@ def compare_entities(entity0: Optional[Entity] = None, # - Should there be a more detailed comparison of parents without id? # - Revisit filter - do we care about RecordType when matching? # How to treat None? + # - Should matching of parents also take the recordtype into account + # for parents that have a name but no id? # 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 + # - Implement comparison of date where one is a string and the other is + # datetime 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: @@ -264,12 +268,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 +375,7 @@ def compare_entities(entity0: Optional[Entity] = None, # compare properties for prop in entity0.properties: - matching = entity1.properties.filter(prop, 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 dd0718c79717bb7a983d8da2b59b9c73ecbd96f3..1dbeb802311c7afaea2340af15e49537520ef57f 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -40,22 +40,22 @@ import sys import warnings from builtins import str from copy import deepcopy -from enum import Enum from datetime import date, datetime +from enum import Enum from functools import cmp_to_key from hashlib import sha512 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 +from typing import TYPE_CHECKING, Any, Final, Literal, Optional, TextIO, Union if TYPE_CHECKING: - from .datatype import DATATYPE - from tempfile import _TemporaryFileWrapper from io import BufferedWriter from os import PathLike + from tempfile import _TemporaryFileWrapper + + from .datatype import DATATYPE QueryDict = dict[str, Optional[str]] from warnings import warn @@ -65,36 +65,17 @@ from lxml import etree from ..configuration import get_config from ..connection.connection import get_connection from ..connection.encode import MultipartParam, multipart_encode -from ..exceptions import ( - AmbiguousEntityError, - AuthorizationError, - ConsistencyError, - EmptyUniqueQueryError, - EntityDoesNotExistError, - EntityError, - EntityHasNoAclError, - EntityHasNoDatatypeError, - HTTPURITooLongError, - LinkAheadConnectionError, - LinkAheadException, - MismatchingEntitiesError, - PagingConsistencyError, - QueryNotUniqueError, - TransactionError, - UniqueNamesError, - UnqualifiedParentsError, - UnqualifiedPropertiesError, -) -from .datatype import ( - BOOLEAN, - DATETIME, - DOUBLE, - INTEGER, - TEXT, - get_list_datatype, - is_list_datatype, - is_reference, -) +from ..exceptions import (AmbiguousEntityError, AuthorizationError, + ConsistencyError, EmptyUniqueQueryError, + EntityDoesNotExistError, EntityError, + EntityHasNoAclError, EntityHasNoDatatypeError, + HTTPURITooLongError, LinkAheadConnectionError, + LinkAheadException, MismatchingEntitiesError, + PagingConsistencyError, QueryNotUniqueError, + TransactionError, UniqueNamesError, + UnqualifiedParentsError, UnqualifiedPropertiesError) +from .datatype import (BOOLEAN, DATETIME, DOUBLE, INTEGER, TEXT, + get_list_datatype, is_list_datatype, is_reference) from .state import State from .timezone import TimeZone from .utils import uuid, xml2str @@ -2522,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 ------- @@ -2561,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): ''' @@ -2693,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 ------ @@ -2721,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 ------- @@ -2732,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]): """ @@ -5498,24 +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, entity: Optional[Entity] = None, pid: Union[None, str, int] = None, - name: Optional[str] = None, check_wrapped: bool = True) -> 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: """ - Return all elements from the given list that match the selection criteria. + Returns a subset of entities from the list based on whether their id and + name matches the selection criterion. + + 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. - 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, 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, only - elements matching both name and ID of the Entity are returned, as long as - name and ID are both set. + 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 - 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. + As IDs can be strings, integer IDs are cast to string for the comparison. Params ------ @@ -5528,10 +5504,9 @@ def _filter_entity_list(listobject, entity: Optional[Entity] = None, pid: Union[ 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. + conjunction : bool, defaults to False + Set to true to return only entities that match both id + and name if both are given. Returns ------- @@ -5539,57 +5514,40 @@ def _filter_entity_list(listobject, entity: Optional[Entity] = None, pid: Union[ 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.") + raise ValueError("If an entity is given, pid and name must not be set.") pid = entity.id name = entity.name - match_entity = True + 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 = [] - 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 + for candidate in listobject: + 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 - # 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: + # If the criteria are satisfied, append the match. + if pid_match and name_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)) + 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 5b99ebc335ed2cb1dfffcda18e6d35d06f3a559d..2fb946c518d940bd505622284070a0f5fafdf12f 100644 --- a/unittests/test_apiutils.py +++ b/unittests/test_apiutils.py @@ -261,17 +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() - prop1 = db.Property() 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 @@ -336,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) @@ -408,10 +407,10 @@ def test_compare_entities_battery(): 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)) + assert empty_diff(db.Property().add_property(prop2), + db.Property().add_property(prop2)) + assert not empty_diff(db.Property().add_property(prop2, importance=db.SUGGESTED), + db.Property().add_property(prop2, importance=db.OBLIGATORY)) # Mixed Lists assert empty_diff(db.Property(value=[1, 2, 'a', r1]), db.Property(value=[1, 2, 'a', r1])) @@ -435,7 +434,7 @@ def test_compare_entities_battery(): 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) + t7 = db.Property(**prop_settings).add_parent(par1).add_property(prop2) 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] @@ -444,6 +443,7 @@ def test_compare_entities_battery(): "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)) + # Order invariance for multi-property - either both fail or same result try: diffs_0 = compare_entities(t1, t2), compare_entities(t1, t2, True) except Exception as e: @@ -453,6 +453,28 @@ def test_compare_entities_battery(): except Exception as e: diffs_1 = type(e) assert diffs_0 == diffs_1 + # Property types + t09, t10 = db.RecordType(), db.RecordType() + for t, ex in [(db.INTEGER, [-12, 0]), (db.DATETIME, ["2030-01-01", "1012-02-29"]), + (db.DOUBLE, [13.23, 7.1]), (db.BOOLEAN, [True, False])]: + t09.add_property(db.Property(name=f"{t}:{ex[0]}", datatype=t, value=ex[0])) + t10.add_property(db.Property(name=f"{t}:{ex[0]}", datatype=t, value=ex[0])) + t09.add_property(name=f"{t}:{ex[1]}", datatype=t, value=ex[1]) + t10.add_property(name=f"{t}:{ex[1]}", datatype=t, value=ex[1]) + assert empty_diff(t09, t10) + t09.add_property(name=f"diff", value=1) + t10.add_property(name=f"diff", value=2) + assert not empty_diff(t09, t10) + # Default values + t09, t10 = db.Record(), db.Record() + t09.add_property(db.Property(name=f"A1"), value="A") + t10.add_property(name=f"A1", value="A") + t09.add_property(db.Property(id=12, name=f"A2"), value="A") + t10.add_property(id=12, name=f"A2", value="A") + t09.add_property(db.Property(id=15), value="A") + t10.add_property(id=15, value="A") + assert empty_diff(t09, t10) + # ToDo: extended tests for references def test_compare_special_properties(): diff --git a/unittests/test_entity.py b/unittests/test_entity.py index 66cffbefe207820ddde1463f62a88788beb7df9a..2127ce028f4de55b8ef0ca704c1e69959c24ba82 100644 --- a/unittests/test_entity.py +++ b/unittests/test_entity.py @@ -25,14 +25,14 @@ import os # pylint: disable=missing-docstring import unittest -from pytest import raises import linkahead -from linkahead import (INTEGER, Entity, Property, Record, RecordType, Parent, +from linkahead import (INTEGER, Entity, Parent, Property, Record, RecordType, configure_connection) from linkahead.common.models import SPECIAL_ATTRIBUTES from linkahead.connection.mockup import MockUpServerConnection from lxml import etree +from pytest import raises UNITTESTDIR = os.path.dirname(os.path.abspath(__file__)) @@ -164,13 +164,13 @@ def test_property_list(): def test_filter(): rt1 = RecordType(id=100) rt2 = RecordType(id=101, name="RT") - rt3 = RecordType() + rt3 = RecordType(name="") 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() + p6 = Property(name="") r1 = Record(id=100) r2 = Record(id=100) r3 = Record(id=101, name="RT") @@ -180,61 +180,105 @@ def test_filter(): 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) + 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 + 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)]: + # Checks with each type + t, t_props, t_pars = entity, entity.properties, entity.parents # Properties # Basic Checks t.add_property(p1) + tp1 = t.properties[-1] 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") + tp3 = t.properties[-1] + assert len(t_props.filter(pid=100)) == 1 + assert tp1 in t_props.filter(pid=100) + assert len(t_props.filter(pid="100")) == 1 + assert tp1 in t_props.filter(pid="100") + assert len(t_props.filter(pid=101, name="RT")) == 1 + assert tp3 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 tp1 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") + tp2 = t_props[-1] + assert tp2 in t_props.filter(pid=100) + assert tp2 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 tp2 in t_props.filter(entity) # Parents # Filtering with both name and id t.add_parent(r3) + tr3 = t.parents[-1] 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") + tr5 = t.parents[-1] + assert tr3 in t_pars.filter(pid=101) + assert tr5 not in t_pars.filter(pid=101) + 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 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") # Works also without id / name and with duplicate parents for ent in test_ents: t.add_parent(ent) - for ent in test_ents: + for ent in t_pars: 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") + + # Grid-Based + r7 = Record() + r7.add_property(Property()).add_property(name="A").add_property(name="B") + r7.add_property(id=27).add_property(id=27, name="A").add_property(id=27, name="B") + r7.add_property(id=43).add_property(id=43, name="A").add_property(id=43, name="B") + assert len(r7.properties.filter(pid=27)) == 3 + assert len(r7.properties.filter(pid=43)) == 3 + assert len(r7.properties.filter(pid=43, conjunction=True)) == 3 + assert len(r7.properties.filter(name="A")) == 3 + assert len(r7.properties.filter(name="B")) == 3 + assert len(r7.properties.filter(name="B", conjunction=True)) == 3 + assert len(r7.properties.filter(pid=1, name="A")) == 1 + assert len(r7.properties.filter(pid=1, name="A", conjunction=True)) == 0 + assert len(r7.properties.filter(pid=27, name="B")) == 4 + assert len(r7.properties.filter(pid=27, name="B", conjunction=True)) == 1 + assert len(r7.properties.filter(pid=27, name="C")) == 3 + assert len(r7.properties.filter(pid=27, name="C", conjunction=True)) == 0 + # Entity based filtering behaves the same + assert (r7.properties.filter(pid=27) == + r7.properties.filter(Property(id=27))) + assert (r7.properties.filter(pid=43, conjunction=True) == + r7.properties.filter(Property(id=43), conjunction=True)) + assert (r7.properties.filter(name="A") == + r7.properties.filter(Property(name="A"))) + assert (r7.properties.filter(name="B") == + r7.properties.filter(Property(name="B"))) + assert (r7.properties.filter(name="B", conjunction=True) == + r7.properties.filter(Property(name="B"), conjunction=True)) + assert (r7.properties.filter(pid=1, name="A") == + r7.properties.filter(Property(id=1, name="A"))) + assert (r7.properties.filter(pid=1, name="A", conjunction=True) == + r7.properties.filter(Property(id=1, name="A"), conjunction=True)) + assert (r7.properties.filter(pid=27, name="B") == + r7.properties.filter(Property(id=27, name="B"))) + assert (r7.properties.filter(pid=27, name="B", conjunction=True) == + r7.properties.filter(Property(id=27, name="B"), conjunction=True)) + assert (r7.properties.filter(pid=27, name="C") == + r7.properties.filter(Property(id=27, name="C"))) + assert (r7.properties.filter(pid=27, name="C", conjunction=True) == + r7.properties.filter(Property(id=27, name="C"), conjunction=True)) + # Name only matching and name overwrite + r8 = Record().add_property(name="A").add_property(name="B").add_property(name="B") + r8.add_property(Property(name="A"), name="B") + r8.add_property(Property(name="A", id=12), name="C") + assert len(r8.properties.filter(name="A")) == 1 + assert len(r8.properties.filter(name="B")) == 3 + assert len(r8.properties.filter(name="C")) == 1 + assert len(r8.properties.filter(pid=12)) == 1