Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions khard/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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\"."""

Copy link
Copy Markdown
Owner

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.


def __init__(self, first: Query, second: Query, *queries: Query) -> None:
self._queries = (first, second, *queries)
Expand Down Expand Up @@ -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\"."""

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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)
Expand Down Expand Up @@ -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]"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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."""
Expand Down Expand Up @@ -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)
53 changes: 53 additions & 0 deletions test/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
NullQuery,
OrQuery,
TermQuery,
UidQuery,
parse,
)

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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.

Loading