-
Notifications
You must be signed in to change notification settings - Fork 70
Fix uid: search to match by prefix instead of substring #357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -167,7 +167,7 @@ def __str__(self) -> str: | |
|
|
||
| class AndQuery(Query): | ||
|
|
||
| """A query to combine multiple queries with "and".""" | ||
| """A query to combine multiple queries with \"and\".""" | ||
|
|
||
| def __init__(self, first: Query, second: Query, *queries: Query) -> None: | ||
| self._queries = (first, second, *queries) | ||
|
|
@@ -198,7 +198,7 @@ def __str__(self) -> str: | |
|
|
||
| class OrQuery(Query): | ||
|
|
||
| """A query to combine multiple queries with "or".""" | ||
| """A query to combine multiple queries with \"or\".""" | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see above |
||
|
|
||
| def __init__(self, first: Query, second: Query, *queries: Query) -> None: | ||
| self._queries = (first, second, *queries) | ||
|
|
@@ -254,6 +254,34 @@ def __str__(self) -> str: | |
| return 'name:{}'.format(self._term) | ||
|
|
||
|
|
||
| class UidQuery(FieldQuery): | ||
|
|
||
| """A query to match the UID field using prefix matching. | ||
|
|
||
| khard list shows a short unique UID prefix for each contact. This | ||
| query matches only contacts whose UID starts with the given term so | ||
| that the displayed prefix selects exactly the expected contact. | ||
| """ | ||
|
|
||
| def __init__(self, value: str) -> None: | ||
| super().__init__("uid", value) | ||
|
|
||
| def _match_union(self, value: "str | datetime | list | dict[str, Any]" | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove the quotes around the type. It works without them as none of these types has to be lazy loaded. See the other _match_union implementations. |
||
| ) -> bool: | ||
| if isinstance(value, str): | ||
| return value.lower().startswith(self._term) | ||
| return super()._match_union(value) | ||
|
|
||
| def __eq__(self, other: object) -> bool: | ||
| return isinstance(other, UidQuery) and self._term == other._term | ||
|
|
||
| def __hash__(self) -> int: | ||
| return hash((UidQuery, self._term)) | ||
|
|
||
| def __str__(self) -> str: | ||
| return 'uid:{}'.format(self._term) | ||
|
|
||
|
|
||
| class PhoneNumberQuery(FieldQuery): | ||
|
|
||
| """A special query to match against phone numbers.""" | ||
|
|
@@ -353,6 +381,8 @@ def parse(string: str) -> TermQuery | FieldQuery: | |
| if kind.startswith(term.lower()): | ||
| return FieldQuery(field, kind) | ||
| return TermQuery(string) | ||
| if field == "uid": | ||
| return UidQuery(term) | ||
| if field in contacts.Contact.get_properties(): | ||
| return FieldQuery(field, term) | ||
| return TermQuery(string) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| NullQuery, | ||
| OrQuery, | ||
| TermQuery, | ||
| UidQuery, | ||
| parse, | ||
| ) | ||
|
|
||
|
|
@@ -203,6 +204,52 @@ def test_kind_query_with_explicit_mismatch(self): | |
| self.assertFalse(query.match(contact)) | ||
|
|
||
|
|
||
| class TestUidQuery(unittest.TestCase): | ||
| def setUp(self): | ||
| self.uid_a = "aaabbbb" | ||
| self.uid_b = "bbbaaaa" | ||
| self.contact_a = TestContact(uid=self.uid_a) | ||
| self.contact_b = TestContact(uid=self.uid_b) | ||
|
|
||
| def test_matches_uid_by_prefix(self): | ||
| q = UidQuery("a") | ||
| self.assertTrue(q.match(self.contact_a)) | ||
|
|
||
| def test_does_not_match_uid_with_term_as_substring_only(self): | ||
| # "a" is a substring of "bbbaaaa" but not a prefix | ||
| q = UidQuery("a") | ||
| self.assertFalse(q.match(self.contact_b)) | ||
|
|
||
| def test_matches_full_uid(self): | ||
| q = UidQuery(self.uid_a) | ||
| self.assertTrue(q.match(self.contact_a)) | ||
| self.assertFalse(q.match(self.contact_b)) | ||
|
|
||
| def test_matching_is_case_insensitive(self): | ||
| q = UidQuery("AAA") | ||
| self.assertTrue(q.match(self.contact_a)) | ||
| self.assertFalse(q.match(self.contact_b)) | ||
|
|
||
| def test_empty_term_matches_any_contact_with_uid_set(self): | ||
| q = UidQuery("") | ||
| self.assertTrue(q.match(self.contact_a)) | ||
| self.assertTrue(q.match(self.contact_b)) | ||
|
|
||
| def test_empty_term_does_not_match_contact_without_uid(self): | ||
| q = UidQuery("") | ||
| self.assertFalse(q.match(TestContact())) | ||
|
|
||
| def test_shared_prefix_can_be_disambiguated(self): | ||
| uid_a = "aaabbbb" | ||
| uid_a2 = "aaacccc" | ||
| contact_a = TestContact(uid=uid_a) | ||
| contact_a2 = TestContact(uid=uid_a2) | ||
| self.assertTrue(UidQuery("aaab").match(contact_a)) | ||
| self.assertFalse(UidQuery("aaab").match(contact_a2)) | ||
| self.assertFalse(UidQuery("aaac").match(contact_a)) | ||
| self.assertTrue(UidQuery("aaac").match(contact_a2)) | ||
|
|
||
|
|
||
| class TestNameQuery(unittest.TestCase): | ||
| def test_matches_formatted_name_field(self): | ||
| vcard = load_contact("minimal.vcf") | ||
|
|
@@ -271,3 +318,9 @@ def test_kind_queries_only_need_a_substring_of_the_enum(self): | |
| self.assertEqual(parse("kind:i"), FieldQuery("kind", "individual")) | ||
| self.assertEqual(parse("kind:org"), FieldQuery("kind", "org")) | ||
| self.assertEqual(parse("kind:o"), FieldQuery("kind", "org")) | ||
|
|
||
| def test_uid_field_creates_uid_query_instance(self): | ||
| actual = parse("uid:abc123") | ||
| expected = UidQuery("abc123") | ||
| self.assertEqual(actual, expected) | ||
| self.assertEqual(type(actual), UidQuery) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you test for equality I think you do not need to test the type again afterwards. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The backslash is not needed inside triple quotes. Please remove it.