Skip to content

Commit 4be3cb2

Browse files
committed
Refactor _get_multi_property into get_all; make name getters nullable
The _list attribute on the underlying vcard object should be faster than iterating through all attributes ourself. With this it should be possible to detect if an attribute is present on the vcard or not. If it is not present the @Property on the wrapper returns None or an empty list or an empty dict, depending on the attribute.
1 parent 3c07f98 commit 4be3cb2

File tree

2 files changed

+75
-58
lines changed

2 files changed

+75
-58
lines changed

khard/contacts.py

Lines changed: 62 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
logger = logging.getLogger(__name__)
4141
T = TypeVar("T")
42+
LabeledStrs = list[Union[str, dict[str, str]]]
4243

4344

4445
@overload
@@ -132,17 +133,15 @@ def get_first(self, property: str) -> Union[None, str, vobject.vcard.Name,
132133
str.
133134
134135
:param property: the field value to get
135-
:param default: the value to return if the vCard does not have this
136-
property
137-
:returns: the property value or the default
136+
:returns: the property value or None
138137
"""
139138
try:
140139
return getattr(self.vcard, property).value
141140
except AttributeError:
142141
return None
143142

144-
def _get_multi_property(self, name: str) -> list:
145-
"""Get a vCard property that can exist more than once.
143+
def get_all(self, name: str) -> list:
144+
"""Get all values of the given vCard property.
146145
147146
It does not matter what the individual vcard properties store as their
148147
value. This function returns them untouched inside an aggregating
@@ -154,14 +153,12 @@ def _get_multi_property(self, name: str) -> list:
154153
:param name: the name of the property (should be UPPER case)
155154
:returns: the values from all occurrences of the named property
156155
"""
157-
values = []
158-
for child in self.vcard.getChildren():
159-
if child.name == name:
160-
ablabel = self._get_ablabel(child)
161-
if ablabel:
162-
values.append({ablabel: child.value})
163-
else:
164-
values.append(child.value)
156+
try:
157+
values = getattr(self.vcard, f"{name.lower()}_list")
158+
except AttributeError:
159+
return []
160+
values = [{label: item.value} if (label := self._get_ablabel(item))
161+
else item.value for item in values]
165162
return sorted(values, key=multi_property_key)
166163

167164
def _delete_vcard_object(self, name: str) -> None:
@@ -585,12 +582,16 @@ def get_last_name_first_name(self) -> str:
585582
return self.formatted_name
586583

587584
@property
588-
def first_name(self) -> str:
589-
return list_to_string(self._get_first_names(), " ")
585+
def first_name(self) -> Optional[str]:
586+
if parts := self._get_first_names():
587+
return list_to_string(parts, " ")
588+
return None
590589

591590
@property
592-
def last_name(self) -> str:
593-
return list_to_string(self._get_last_names(), " ")
591+
def last_name(self) -> Optional[str]:
592+
if parts := self._get_last_names():
593+
return list_to_string(parts, " ")
594+
return None
594595

595596
def _add_name(self, prefix: StrList, first_name: StrList,
596597
additional_name: StrList, last_name: StrList,
@@ -617,7 +618,7 @@ def organisations(self) -> list[Union[list[str], dict[str, list[str]]]]:
617618
"""
618619
:returns: list of organisations, sorted alphabetically
619620
"""
620-
return self._get_multi_property("ORG")
621+
return self.get_all("org")
621622

622623
def _add_organisation(self, organisation: StrList, label: Optional[str] = None) -> None:
623624
"""Add one ORG entry to the underlying vcard
@@ -640,48 +641,47 @@ def _add_organisation(self, organisation: StrList, label: Optional[str] = None)
640641
showas_obj.value = "COMPANY"
641642

642643
@property
643-
def titles(self) -> list[Union[str, dict[str, str]]]:
644-
return self._get_multi_property("TITLE")
644+
def titles(self) -> LabeledStrs:
645+
return self.get_all("title")
645646

646647
def _add_title(self, title: str, label: Optional[str] = None) -> None:
647648
self._add_labelled_property("title", title, label, True)
648649

649650
@property
650-
def roles(self) -> list[Union[str, dict[str, str]]]:
651-
return self._get_multi_property("ROLE")
651+
def roles(self) -> LabeledStrs:
652+
return self.get_all("role")
652653

653654
def _add_role(self, role: str, label: Optional[str] = None) -> None:
654655
self._add_labelled_property("role", role, label, True)
655656

656657
@property
657-
def nicknames(self) -> list[Union[str, dict[str, str]]]:
658-
return self._get_multi_property("NICKNAME")
658+
def nicknames(self) -> LabeledStrs:
659+
return self.get_all("nickname")
659660

660661
def _add_nickname(self, nickname: str, label: Optional[str] = None) -> None:
661662
self._add_labelled_property("nickname", nickname, label, True)
662663

663664
@property
664-
def notes(self) -> list[Union[str, dict[str, str]]]:
665-
return self._get_multi_property("NOTE")
665+
def notes(self) -> LabeledStrs:
666+
return self.get_all("note")
666667

667668
def _add_note(self, note: str, label: Optional[str] = None) -> None:
668669
self._add_labelled_property("note", note, label, True)
669670

670671
@property
671-
def webpages(self) -> list[Union[str, dict[str, str]]]:
672-
return self._get_multi_property("URL")
672+
def webpages(self) -> LabeledStrs:
673+
return self.get_all("url")
673674

674675
def _add_webpage(self, webpage: str, label: Optional[str] = None) -> None:
675676
self._add_labelled_property("url", webpage, label, True)
676677

677678
@property
678679
def categories(self) -> Union[list[str], list[list[str]]]:
679-
category_list = []
680-
for child in self.vcard.getChildren():
681-
if child.name == "CATEGORIES":
682-
value = child.value
683-
category_list.append(
684-
value if isinstance(value, list) else [value])
680+
category_list = self.get_all("categories")
681+
if not category_list:
682+
return category_list
683+
category_list = [value if isinstance(value, list) else [value] for
684+
value in category_list]
685685
if len(category_list) == 1:
686686
return category_list[0]
687687
return sorted(category_list)
@@ -766,13 +766,16 @@ def emails(self) -> dict[str, list[str]]:
766766
:returns: dict of type and email address list
767767
"""
768768
email_dict: dict[str, list[str]] = {}
769-
for child in self.vcard.getChildren():
770-
if child.name == "EMAIL":
771-
type = list_to_string(
772-
self._get_types_for_vcard_object(child, "internet"), ", ")
773-
if type not in email_dict:
774-
email_dict[type] = []
775-
email_dict[type].append(child.value)
769+
try:
770+
emails = self.vcard.email_list
771+
except AttributeError:
772+
return {}
773+
for child in emails:
774+
type = list_to_string(
775+
self._get_types_for_vcard_object(child, "internet"), ", ")
776+
if type not in email_dict:
777+
email_dict[type] = []
778+
email_dict[type].append(child.value)
776779
# sort email address lists
777780
for email_list in email_dict.values():
778781
email_list.sort()
@@ -817,19 +820,22 @@ def post_addresses(self) -> dict[str, list[PostAddress]]:
817820
:returns: dict of type and post address list
818821
"""
819822
post_adr_dict: dict[str, list[PostAddress]] = {}
820-
for child in self.vcard.getChildren():
821-
if child.name == "ADR":
822-
type = list_to_string(self._get_types_for_vcard_object(
823-
child, "home"), ", ")
824-
if type not in post_adr_dict:
825-
post_adr_dict[type] = []
826-
post_adr_dict[type].append({"box": child.value.box,
827-
"extended": child.value.extended,
828-
"street": child.value.street,
829-
"code": child.value.code,
830-
"city": child.value.city,
831-
"region": child.value.region,
832-
"country": child.value.country})
823+
try:
824+
addresses = self.vcard.adr_list
825+
except AttributeError:
826+
return {}
827+
for child in addresses:
828+
type = list_to_string(self._get_types_for_vcard_object(
829+
child, "home"), ", ")
830+
if type not in post_adr_dict:
831+
post_adr_dict[type] = []
832+
post_adr_dict[type].append({"box": child.value.box,
833+
"extended": child.value.extended,
834+
"street": child.value.street,
835+
"code": child.value.code,
836+
"city": child.value.city,
837+
"region": child.value.region,
838+
"country": child.value.country})
833839
# sort post address lists
834840
for post_adr_list in post_adr_dict.values():
835841
post_adr_list.sort(key=lambda x: (
@@ -942,9 +948,9 @@ def __init__(self, vcard: vobject.base.Component,
942948
# getters and setters
943949
#####################
944950

945-
def _get_private_objects(self) -> dict[str, list[Union[str, dict[str, str]]]]:
951+
def _get_private_objects(self) -> dict[str, LabeledStrs]:
946952
supported = [x.lower() for x in self.supported_private_objects]
947-
private_objects: dict[str, list[Union[str, dict[str, str]]]] = {}
953+
private_objects: dict[str, LabeledStrs] = {}
948954
for child in self.vcard.getChildren():
949955
lower = child.name.lower()
950956
if lower.startswith("x-") and lower[2:] in supported:

test/test_vcard_wrapper.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ class AddLabelledObject(unittest.TestCase):
510510
def assertTitle(self, expected):
511511
wrapper = TestVCardWrapper()
512512
yield wrapper
513-
self.assertEqual(wrapper._get_multi_property("TITLE"), expected)
513+
self.assertEqual(wrapper.get_all("title"), expected)
514514

515515
def test_add_a_string(self):
516516
with self.assertTitle(["foo"]) as wrapper:
@@ -578,10 +578,21 @@ def test_can_return_any_value_contradicting_type_annotation(self):
578578
class NullableProperties(unittest.TestCase):
579579
"test that properties that are not present on the vcard return None"
580580

581+
LIST_PROPERTIES = ["categories", "titles", "webpages", "organisations",
582+
"notes", "roles", "nicknames"]
583+
DICT_PROPERTIES = ["post_addresses", "emails", "phone_numbers"]
584+
BASE_PROPERTIES = ["formatted_name", "kind", "version"]
585+
581586
def test_properties(self):
582587
for version in ["3.0", "4.0"]:
583588
card = TestVCardWrapper(version=version)
584589
for property in Contact.get_properties():
585-
if property not in ["formatted_name", "version", "kind"]:
590+
if property in self.DICT_PROPERTIES:
591+
with self.subTest(property=property, version=version):
592+
self.assertEqual(getattr(card, property), {})
593+
elif property in self.LIST_PROPERTIES:
594+
with self.subTest(property=property, version=version):
595+
self.assertEqual(getattr(card, property), [])
596+
elif property not in self.BASE_PROPERTIES:
586597
with self.subTest(property=property, version=version):
587598
self.assertIsNone(getattr(card, property))

0 commit comments

Comments
 (0)