diff --git a/khard/actions.py b/khard/actions.py index af2e119..cd4b9ce 100644 --- a/khard/actions.py +++ b/khard/actions.py @@ -1,6 +1,8 @@ """Names and aliases for the subcommands on the command line""" -from typing import Dict, Generator, Iterable, List, Optional +from __future__ import annotations + +from typing import Generator, Iterable class Actions: @@ -8,7 +10,7 @@ class Actions: """A class to manage the names and aliases of the command line subcommands.""" - action_map: Dict[str, List[str]] = { + action_map: dict[str, list[str]] = { "add-email": [], "addressbooks": ["abooks"], "birthdays": ["bdays"], @@ -28,7 +30,7 @@ class Actions: } @classmethod - def get_action(cls, alias: str) -> Optional[str]: + def get_action(cls, alias: str) -> None | str: """Find the name of the action for the supplied alias. If no action is associated with the given alias, None is returned. @@ -42,7 +44,7 @@ def get_action(cls, alias: str) -> Optional[str]: return None @classmethod - def get_aliases(cls, action: str) -> List[str]: + def get_aliases(cls, action: str) -> list[str]: """Find all aliases for the given action. If there is no such action, None is returned. diff --git a/khard/address_book.py b/khard/address_book.py index f015f15..bb84c16 100644 --- a/khard/address_book.py +++ b/khard/address_book.py @@ -1,12 +1,14 @@ """A simple class to load and manage the vcard files from disk.""" +from __future__ import annotations + import abc import binascii from collections.abc import Mapping, Sequence import glob import logging import os -from typing import Dict, Generator, Iterator, List, Optional, Union, overload +from typing import Generator, Iterator, overload import vobject.base @@ -42,9 +44,8 @@ class AddressBook(metaclass=abc.ABCMeta): def __init__(self, name: str) -> None: """:param name: the name to identify the address book""" self._loaded = False - self.contacts: Dict[str, "carddav_object.CarddavObject"] = {} - self._short_uids: Optional[Dict[str, - "carddav_object.CarddavObject"]] = None + self.contacts: dict[str, "carddav_object.CarddavObject"] = {} + self._short_uids: None | dict[str, "carddav_object.CarddavObject"] = None self.name = name def __str__(self) -> str: @@ -83,7 +84,7 @@ def search(self, query: Query) -> Generator["carddav_object.CarddavObject", if query.match(contact): yield contact - def get_short_uid_dict(self, query: Query = AnyQuery()) -> Dict[ + def get_short_uid_dict(self, query: Query = AnyQuery()) -> dict[ str, "carddav_object.CarddavObject"]: """Create a dictionary of shortened UIDs for all contacts. @@ -154,7 +155,7 @@ class VdirAddressBook(AddressBook): """ def __init__(self, name: str, path: str, - private_objects: Optional[List[str]] = None, + private_objects: None | list[str] = None, localize_dates: bool = True, skip: bool = False) -> None: """ :param name: the name to identify the address book @@ -236,7 +237,7 @@ class AddressBookCollection(AddressBook, Mapping, Sequence): this class to use all other methods from the parent AddressBook class. """ - def __init__(self, name: str, abooks: List[VdirAddressBook]) -> None: + def __init__(self, name: str, abooks: list[VdirAddressBook]) -> None: """ :param name: the name to identify the address book :param abooks: a list of address books to combine in this collection @@ -270,11 +271,11 @@ def load(self, query: Query = AnyQuery()) -> None: len(self.contacts), self.name) @overload - def __getitem__(self, key: Union[int, str]) -> VdirAddressBook: ... + def __getitem__(self, key: int | str) -> VdirAddressBook: ... @overload - def __getitem__(self, key: slice) -> List[VdirAddressBook]: ... - def __getitem__(self, key: Union[int, str, slice] - ) -> Union[VdirAddressBook, List[VdirAddressBook]]: + def __getitem__(self, key: slice) -> list[VdirAddressBook]: ... + def __getitem__(self, key: int | str | slice + ) -> VdirAddressBook | list[VdirAddressBook]: """Get one or more of the backing address books by name or index :param key: the name of the address book to get or its index diff --git a/khard/carddav_object.py b/khard/carddav_object.py index 77f9ddb..0780bc1 100644 --- a/khard/carddav_object.py +++ b/khard/carddav_object.py @@ -6,6 +6,8 @@ - version 4.0: https://tools.ietf.org/html/rfc6350 """ +from __future__ import annotations + import copy import datetime import io @@ -15,8 +17,7 @@ import re import sys import time -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, \ - TypeVar, Union, Sequence, overload +from typing import Any, Callable, Literal, TypeVar, Sequence, overload from atomicwrites import atomic_write from ruamel import yaml @@ -36,13 +37,12 @@ @overload -def multi_property_key(item: str) -> Tuple[Literal[0], str]: ... +def multi_property_key(item: str) -> tuple[Literal[0], str]: ... @overload -def multi_property_key(item: Dict[T, Any]) -> Tuple[Literal[1], T]: ... +def multi_property_key(item: dict[T, Any]) -> tuple[Literal[1], T]: ... @overload -def multi_property_key(item: Union[str, Dict[T, Any]]) -> Tuple[Union[Literal[0], Literal[1]], Union[str, T]]: ... -def multi_property_key(item: Union[str, Dict[T, Any]] - ) -> Tuple[int, Union[T, str]]: +def multi_property_key(item: str | dict[T, Any]) -> tuple[Literal[0] | Literal[1], str | T]: ... +def multi_property_key(item: str | dict[T, Any]) -> tuple[int, T | str]: """Key function to pass to sorted(), allowing sorting of dicts with lists and strings. Dicts will be sorted by their label, after other types. @@ -82,7 +82,7 @@ class VCardWrapper: address_types_v4 = ("home", "work") def __init__(self, vcard: vobject.base.Component, - version: Optional[str] = None) -> None: + version: str | None = None) -> None: """Initialize the wrapper around the given vcard. :param vcard: the vCard to wrap @@ -124,7 +124,7 @@ def get_first(self, property: str, default: str = "") -> str: except AttributeError: return default - def _get_multi_property(self, name: str) -> List: + def _get_multi_property(self, name: str) -> list: """Get a vCard property that can exist more than once. It does not matter what the individual vcard properties store as their @@ -172,7 +172,7 @@ def _delete_vcard_object(self, name: str) -> None: @staticmethod def _parse_type_value(types: Sequence[str], supported_types: Sequence[str] - ) -> Tuple[List[str], List[str], int]: + ) -> tuple[list[str], list[str], int]: """Parse type value of phone numbers, email and post addresses. :param types: list of type values @@ -201,7 +201,7 @@ def _parse_type_value(types: Sequence[str], supported_types: Sequence[str] return (standard_types, custom_types, pref) def _get_types_for_vcard_object(self, object: vobject.base.ContentLine, - default_type: str) -> List[str]: + default_type: str) -> list[str]: """get list of types for phone number, email or post address :param object: vcard class object @@ -287,7 +287,7 @@ def _update_revision(self) -> None: rev.value = datetime.datetime.now().strftime("%Y%m%dT%H%M%SZ") @property - def birthday(self) -> Optional[Date]: + def birthday(self) -> Date | None: """Return the birthday as a datetime object or a string depending on whether it is of type text or not. If no birthday is present in the vcard None is returned. @@ -323,7 +323,7 @@ def birthday(self, date: Date) -> None: bday.params['VALUE'] = ['text'] @property - def anniversary(self) -> Optional[Date]: + def anniversary(self) -> Date | None: """ :returns: contacts anniversary or None if not available """ @@ -401,7 +401,7 @@ def _get_new_group(self, group_type: str = "") -> str: return group_name def _add_labelled_property( - self, property: str, value: StrList, label: Optional[str] = None, + self, property: str, value: StrList, label: str | None = None, name_groups: bool = False, allowed_object_type: ObjectType = ObjectType.str) -> None: """Add an object to the VCARD. If a label is given it will be added to @@ -424,8 +424,7 @@ def _add_labelled_property( ablabel_obj.group = group_name ablabel_obj.value = label - def _prepare_birthday_value(self, date: Date) -> Tuple[Optional[str], - bool]: + def _prepare_birthday_value(self, date: Date) -> tuple[str | None, bool]: """Prepare a value to be stored in a BDAY or ANNIVERSARY attribute. :param date: the date like value to be stored @@ -490,7 +489,7 @@ def formatted_name(self, value: str) -> None: final = "" self.vcard.add("FN").value = final - def _get_names_part(self, part: str) -> List[str]: + def _get_names_part(self, part: str) -> list[str]: """Get some part of the "N" entry in the vCard as a list :param part: the name to get e.g. "prefix" or "given" @@ -506,19 +505,19 @@ def _get_names_part(self, part: str) -> List[str]: return [] return the_list if isinstance(the_list, list) else [the_list] - def _get_name_prefixes(self) -> List[str]: + def _get_name_prefixes(self) -> list[str]: return self._get_names_part("prefix") - def _get_first_names(self) -> List[str]: + def _get_first_names(self) -> list[str]: return self._get_names_part("given") - def _get_additional_names(self) -> List[str]: + def _get_additional_names(self) -> list[str]: return self._get_names_part("additional") - def _get_last_names(self) -> List[str]: + def _get_last_names(self) -> list[str]: return self._get_names_part("family") - def _get_name_suffixes(self) -> List[str]: + def _get_name_suffixes(self) -> list[str]: return self._get_names_part("suffix") def get_first_name_last_name(self) -> str: @@ -535,7 +534,7 @@ def get_last_name_first_name(self) -> str: """Compute the full name of the contact by joining the last names and then after a comma the first and additional names together """ - last_names: List[str] = [] + last_names: list[str] = [] if self._get_last_names(): last_names += self._get_last_names() first_and_additional_names = self._get_first_names() + \ @@ -579,13 +578,13 @@ def _add_name(self, prefix: StrList, first_name: StrList, suffix=convert_to_vcard("name suffix", suffix, ObjectType.both)) @property - def organisations(self) -> List[Union[List[str], Dict[str, List[str]]]]: + def organisations(self) -> list[list[str] | dict[str, list[str]]]: """ :returns: list of organisations, sorted alphabetically """ return self._get_multi_property("ORG") - def _add_organisation(self, organisation: StrList, label: Optional[str] = None) -> None: + def _add_organisation(self, organisation: StrList, label: str | None = None) -> None: """Add one ORG entry to the underlying vcard :param organisation: the value to add @@ -606,42 +605,42 @@ def _add_organisation(self, organisation: StrList, label: Optional[str] = None) showas_obj.value = "COMPANY" @property - def titles(self) -> List[Union[str, Dict[str, str]]]: + def titles(self) -> list[str | dict[str, str]]: return self._get_multi_property("TITLE") - def _add_title(self, title: str, label: Optional[str] = None) -> None: + def _add_title(self, title: str, label: str | None = None) -> None: self._add_labelled_property("title", title, label, True) @property - def roles(self) -> List[Union[str, Dict[str, str]]]: + def roles(self) -> list[str | dict[str, str]]: return self._get_multi_property("ROLE") - def _add_role(self, role: str, label: Optional[str] = None) -> None: + def _add_role(self, role: str, label: str | None = None) -> None: self._add_labelled_property("role", role, label, True) @property - def nicknames(self) -> List[Union[str, Dict[str, str]]]: + def nicknames(self) -> list[str | dict[str, str]]: return self._get_multi_property("NICKNAME") - def _add_nickname(self, nickname: str, label: Optional[str] = None) -> None: + def _add_nickname(self, nickname: str, label: str | None = None) -> None: self._add_labelled_property("nickname", nickname, label, True) @property - def notes(self) -> List[Union[str, Dict[str, str]]]: + def notes(self) -> list[str | dict[str, str]]: return self._get_multi_property("NOTE") - def _add_note(self, note: str, label: Optional[str] = None) -> None: + def _add_note(self, note: str, label: str | None = None) -> None: self._add_labelled_property("note", note, label, True) @property - def webpages(self) -> List[Union[str, Dict[str, str]]]: + def webpages(self) -> list[str | dict[str, str]]: return self._get_multi_property("URL") - def _add_webpage(self, webpage: str, label: Optional[str] = None) -> None: + def _add_webpage(self, webpage: str, label: str | None = None) -> None: self._add_labelled_property("url", webpage, label, True) @property - def categories(self) -> Union[List[str], List[List[str]]]: + def categories(self) -> list[str] | list[list[str]]: category_list = [] for child in self.vcard.getChildren(): if child.name == "CATEGORIES": @@ -652,7 +651,7 @@ def categories(self) -> Union[List[str], List[List[str]]]: return category_list[0] return sorted(category_list) - def _add_category(self, categories: List[str]) -> None: + def _add_category(self, categories: list[str]) -> None: """Add categories to the vCard :param categories: @@ -662,11 +661,11 @@ def _add_category(self, categories: List[str]) -> None: ObjectType.list) @property - def phone_numbers(self) -> Dict[str, List[str]]: + def phone_numbers(self) -> dict[str, list[str]]: """ :returns: dict of type and phone number list """ - phone_dict: Dict[str, List[str]] = {} + phone_dict: dict[str, list[str]] = {} for child in self.vcard.getChildren(): if child.name == "TEL": # phone types @@ -727,11 +726,11 @@ def _add_phone_number(self, type: str, number: str) -> None: label_obj.value = custom_types[0] @property - def emails(self) -> Dict[str, List[str]]: + def emails(self) -> dict[str, list[str]]: """ :returns: dict of type and email address list """ - email_dict: Dict[str, List[str]] = {} + email_dict: dict[str, list[str]] = {} for child in self.vcard.getChildren(): if child.name == "EMAIL": type = list_to_string( @@ -778,11 +777,11 @@ def add_email(self, type: str, address: str) -> None: label_obj.value = custom_types[0] @property - def post_addresses(self) -> Dict[str, List[PostAddress]]: + def post_addresses(self) -> dict[str, list[PostAddress]]: """ :returns: dict of type and post address list """ - post_adr_dict: Dict[str, List[PostAddress]] = {} + post_adr_dict: dict[str, list[PostAddress]] = {} for child in self.vcard.getChildren(): if child.name == "ADR": type = list_to_string(self._get_types_for_vcard_object( @@ -803,8 +802,8 @@ def post_addresses(self) -> Dict[str, List[PostAddress]]: list_to_string(x['street'], " ").lower())) return post_adr_dict - def get_formatted_post_addresses(self) -> Dict[str, List[str]]: - formatted_post_adr_dict: Dict[str, List[str]] = {} + def get_formatted_post_addresses(self) -> dict[str, list[str]]: + formatted_post_adr_dict: dict[str, list[str]] = {} for type, post_adr_list in self.post_addresses.items(): formatted_post_adr_dict[type] = [] for post_adr in post_adr_list: @@ -888,8 +887,8 @@ class YAMLEditable(VCardWrapper): """Conversion of vcards to YAML and updating the vcard from YAML""" def __init__(self, vcard: vobject.base.Component, - supported_private_objects: Optional[List[str]] = None, - version: Optional[str] = None, localize_dates: bool = False + supported_private_objects: list[str] | None = None, + version: str | None = None, localize_dates: bool = False ) -> None: """Initialize attributes needed for yaml conversions @@ -908,9 +907,9 @@ def __init__(self, vcard: vobject.base.Component, # getters and setters ##################### - def _get_private_objects(self) -> Dict[str, List[Union[str, Dict[str, str]]]]: + def _get_private_objects(self) -> dict[str, list[str | dict[str, str]]]: supported = [x.lower() for x in self.supported_private_objects] - private_objects: Dict[str, List[Union[str, Dict[str, str]]]] = {} + private_objects: dict[str, list[str | dict[str, str]]] = {} for child in self.vcard.getChildren(): lower = child.name.lower() if lower.startswith("x-") and lower[2:] in supported: @@ -940,7 +939,7 @@ def get_formatted_birthday(self) -> str: ####################### @staticmethod - def _format_date_object(date: Optional[Date], localize: bool) -> str: + def _format_date_object(date: Date | None, localize: bool) -> str: if not date: return "" if isinstance(date, str): @@ -971,7 +970,7 @@ def _filter_invalid_tags(contents: str) -> str: return contents @staticmethod - def _parse_yaml(input: str) -> Dict: + def _parse_yaml(input: str) -> dict: """Parse a YAML document into a dictionary and validate the data to some degree. @@ -997,9 +996,8 @@ def _parse_yaml(input: str) -> Dict: return contact_data @staticmethod - def _set_string_list(setter: Callable[[str, Optional[str]], None], - key: str, data: Dict[str, Union[str, List[str]]] - ) -> None: + def _set_string_list(setter: Callable[[str, str | None], None], + key: str, data: dict[str, str | list[str]]) -> None: """Pre-process a string or list and set each value with the given setter @@ -1026,7 +1024,7 @@ def _set_string_list(setter: Callable[[str, Optional[str]], None], raise ValueError( "{} must be a string or a list of strings".format(key)) - def _set_date(self, target: str, key: str, data: Dict) -> None: + def _set_date(self, target: str, key: str, data: dict) -> None: new = data.get(key) if not new: return @@ -1305,8 +1303,8 @@ class CarddavObject(YAMLEditable): def __init__(self, vcard: vobject.base.Component, address_book: "address_book.VdirAddressBook", filename: str, - supported_private_objects: Optional[List[str]] = None, - vcard_version: Optional[str] = None, + supported_private_objects: list[str] | None = None, + vcard_version: str | None = None, localize_dates: bool = False) -> None: """Initialize the vcard object. @@ -1332,8 +1330,8 @@ def __init__(self, vcard: vobject.base.Component, @classmethod def new(cls, address_book: "address_book.VdirAddressBook", - supported_private_objects: Optional[List[str]] = None, - version: Optional[str] = None, localize_dates: bool = False + supported_private_objects: list[str] | None = None, + version: str | None = None, localize_dates: bool = False ) -> "CarddavObject": """Create a new CarddavObject from scratch""" vcard = vobject.vCard() @@ -1347,8 +1345,8 @@ def new(cls, address_book: "address_book.VdirAddressBook", @classmethod def from_file(cls, address_book: "address_book.VdirAddressBook", filename: str, query: Query = AnyQuery(), - supported_private_objects: Optional[List[str]] = None, - localize_dates: bool = False) -> Optional["CarddavObject"]: + supported_private_objects: list[str] | None = None, + localize_dates: bool = False) -> "CarddavObject | None": """Load a CarddavObject object from a .vcf file if the plain file matches the query. @@ -1379,8 +1377,8 @@ def from_file(cls, address_book: "address_book.VdirAddressBook", @classmethod def from_yaml(cls, address_book: "address_book.VdirAddressBook", yaml: str, - supported_private_objects: Optional[List[str]] = None, - version: Optional[str] = None, localize_dates: bool = False + supported_private_objects: list[str] | None = None, + version: str | None = None, localize_dates: bool = False ) -> "CarddavObject": """Use this if you want to create a new contact from user input.""" contact = cls.new(address_book, supported_private_objects, version, @@ -1531,7 +1529,7 @@ def delete_vcard_file(self) -> None: logger.error("Can not remove vCard file: %s", err) @classmethod - def get_properties(cls) -> List[str]: + def get_properties(cls) -> list[str]: """Return the property names that are defined on this class.""" return [name for name in dir(CarddavObject) if isinstance(getattr(CarddavObject, name), property)] diff --git a/khard/cli.py b/khard/cli.py index 2e87f5d..6d73d45 100644 --- a/khard/cli.py +++ b/khard/cli.py @@ -3,7 +3,6 @@ import argparse import logging import sys -from typing import List, Tuple from .actions import Actions from .carddav_object import CarddavObject @@ -33,7 +32,7 @@ def __init__(self, *choices: str, nested: bool = False) -> None: self._choices = sorted(choices) self._nested = nested - def __call__(self, argument: str) -> List[str]: + def __call__(self, argument: str) -> list[str]: ret = [] for candidate in argument.split(","): candidate = candidate.lower() @@ -48,7 +47,7 @@ def __call__(self, argument: str) -> List[str]: return ret -def create_parsers() -> Tuple[argparse.ArgumentParser, +def create_parsers() -> tuple[argparse.ArgumentParser, argparse.ArgumentParser]: """Create two argument parsers. @@ -349,7 +348,7 @@ def create_parsers() -> Tuple[argparse.ArgumentParser, return first_parser, parser -def parse_args(argv: List[str]) -> Tuple[argparse.Namespace, Config]: +def parse_args(argv: list[str]) -> tuple[argparse.Namespace, Config]: """Parse the command line arguments and return the namespace that was creates by argparse.ArgumentParser.parse_args(). @@ -437,7 +436,7 @@ def merge_args_into_config(args: argparse.Namespace, config: Config) -> Config: return config -def init(argv: List[str]) -> Tuple[argparse.Namespace, Config]: +def init(argv: list[str]) -> tuple[argparse.Namespace, Config]: """Initialize khard by parsing the command line and reading the config file :param argv: the command line arguments diff --git a/khard/config.py b/khard/config.py index a6216a5..9897c8d 100644 --- a/khard/config.py +++ b/khard/config.py @@ -1,5 +1,7 @@ """Loading and validation of the configuration file""" +from __future__ import annotations + from argparse import Namespace import io import locale @@ -7,7 +9,7 @@ import os import re import shlex -from typing import Iterable, Dict, List, Optional, Union +from typing import Iterable import configobj try: @@ -26,14 +28,14 @@ # This is the type of the config file parameter accepted by the configobj # library: # https://configobj.readthedocs.io/en/latest/configobj.html#reading-a-config-file -ConfigFile = Union[str, List[str], io.StringIO] +ConfigFile = str | list[str] | io.StringIO class ConfigError(Exception): """Errors during config file parsing""" -def validate_command(value: List[str]) -> List[str]: +def validate_command(value: list[str]) -> list[str]: """Special validator to check shell commands The input must either be a list of strings or a string that shlex.split can @@ -68,7 +70,7 @@ def validate_action(value: str) -> str: return validate.is_option(value, *Actions.get_actions()) -def validate_private_objects(value: List[str]) -> List[str]: +def validate_private_objects(value: list[str]) -> list[str]: """Check that the private objects are reasonable :param value: the config value to check @@ -93,7 +95,7 @@ class Config: supported_vcard_versions = ("3.0", "4.0") - def __init__(self, config_file: Optional[ConfigFile] = None) -> None: + def __init__(self, config_file: ConfigFile | None = None) -> None: self.config: configobj.ConfigObj self.abooks: AddressBookCollection locale.setlocale(locale.LC_ALL, '') @@ -102,7 +104,7 @@ def __init__(self, config_file: Optional[ConfigFile] = None) -> None: self._set_attributes() @classmethod - def _load_config_file(cls, config_file: Optional[ConfigFile] + def _load_config_file(cls, config_file: ConfigFile | None ) -> configobj.ConfigObj: """Find and load the config file. @@ -188,7 +190,7 @@ def init_address_books(self) -> None: except OSError as err: raise ConfigError(str(err)) - def get_address_books(self, names: Iterable[str], queries: Dict[str, Query] + def get_address_books(self, names: Iterable[str], queries: dict[str, Query] ) -> AddressBookCollection: """Load all address books with the given names. @@ -212,7 +214,7 @@ def get_address_books(self, names: Iterable[str], queries: Dict[str, Query] abook.load(queries[abook.name], self.search_in_source_files) return collection - def merge(self, other: Union[configobj.ConfigObj, Dict]) -> None: + def merge(self, other: configobj.ConfigObj | dict) -> None: """Merge the config with some other dict or ConfigObj :param other: the other dict or ConfigObj to merge into self diff --git a/khard/formatter.py b/khard/formatter.py index 9ed4f57..7470114 100644 --- a/khard/formatter.py +++ b/khard/formatter.py @@ -1,7 +1,5 @@ """Formatting and sorting of contacts""" -from typing import Dict, List - from .carddav_object import CarddavObject @@ -17,8 +15,8 @@ class Formatter: LAST = "last_name" FORMAT = "formatted_name" - def __init__(self, display: str, preferred_email: List[str], - preferred_phone: List[str], show_nicknames: bool, + def __init__(self, display: str, preferred_email: list[str], + preferred_phone: list[str], show_nicknames: bool, parsable: bool) -> None: self._display = display self._preferred_email = preferred_email @@ -27,7 +25,7 @@ def __init__(self, display: str, preferred_email: List[str], self._parsable = parsable @staticmethod - def format_labeled_field(field: Dict[str, List[str]], preferred: List[str] + def format_labeled_field(field: dict[str, list[str]], preferred: list[str] ) -> str: """Format a labeled field from a vCard for display, the first entry under the preferred label will be returned diff --git a/khard/helpers/__init__.py b/khard/helpers/__init__.py index e549730..935d413 100644 --- a/khard/helpers/__init__.py +++ b/khard/helpers/__init__.py @@ -1,19 +1,21 @@ """Some helper functions for khard""" +from __future__ import annotations + from datetime import datetime import pathlib import random import string -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Sequence from ruamel.yaml.scalarstring import LiteralScalarString from .typing import list_to_string, PostAddress -YamlPostAddresses = Dict[str, Union[List[Dict[str, Any]], Dict[str, Any]]] +YamlPostAddresses = dict[str, list[dict[str, Any]] | dict[str, Any]] -def pretty_print(table: List[List[str]], justify: str = "L") -> str: +def pretty_print(table: list[list[str]], justify: str = "L") -> str: """Converts a list of lists into a string formatted like a table with spaces separating fields and newlines separating rows""" # support for multiline columns @@ -66,9 +68,8 @@ def get_random_uid() -> str: for _ in range(36)]) -def yaml_clean(value: Union[str, Sequence, Dict[str, Any], None] - ) -> Union[Sequence, str, Dict[str, Any], LiteralScalarString, - None]: +def yaml_clean(value: str | Sequence | dict[str, Any] | None + ) -> Sequence | str | dict[str, Any] | LiteralScalarString | None: """ sanitize yaml values according to some simple principles: 1. empty values are none, so ruamel does not print an empty list/str @@ -95,9 +96,9 @@ def yaml_clean(value: Union[str, Sequence, Dict[str, Any], None] def yaml_dicts( - data: Optional[Dict[str, Any]], - defaults: Union[Dict[str, Any], List[str], None] = None - ) -> Optional[Dict[str, Any]]: + data: dict[str, Any] | None, + defaults: dict[str, Any] | list[str] | None = None + ) -> dict[str, Any] | None: """ format a dict according to template, if empty use specified defaults @@ -118,10 +119,10 @@ def yaml_dicts( return data_dict -def yaml_addresses(addresses: Optional[Dict[str, List[PostAddress]]], - address_properties: List[str], - defaults: Optional[List[str]] = None - ) -> Optional[YamlPostAddresses]: +def yaml_addresses(addresses: dict[str, list[PostAddress]] | None, + address_properties: list[str], + defaults: list[str] | None = None + ) -> YamlPostAddresses | None: """ build a dict from an address, using a list of properties, an address has. @@ -151,8 +152,8 @@ def yaml_addresses(addresses: Optional[Dict[str, List[PostAddress]]], return address_dict -def yaml_anniversary(anniversary: Union[str, datetime, None], - version: str) -> Optional[str]: +def yaml_anniversary(anniversary: str | datetime | None, version: str + ) -> str | None: """ format an anniversary according to its contents and the vCard version. @@ -185,9 +186,9 @@ def yaml_anniversary(anniversary: Union[str, datetime, None], return anniversary -def convert_to_yaml(name: str, value: Union[None, str, List], indentation: int, +def convert_to_yaml(name: str, value: None | str | list, indentation: int, index_of_colon: int, show_multi_line_character: bool - ) -> List[str]: + ) -> list[str]: """converts a value list into yaml syntax :param name: name of object (example: phone) @@ -250,7 +251,7 @@ def convert_to_yaml(name: str, value: Union[None, str, List], indentation: int, return strings -def indent_multiline_string(input: Union[str, List], indentation: int, +def indent_multiline_string(input: str | list, indentation: int, show_multi_line_character: bool) -> str: # if input is a list, convert to string first if isinstance(input, list): @@ -265,7 +266,7 @@ def indent_multiline_string(input: Union[str, List], indentation: int, def get_new_contact_template( - supported_private_objects: Optional[List[str]] = None) -> str: + supported_private_objects: list[str] | None = None) -> str: formatted_private_objects = [] if supported_private_objects: formatted_private_objects.append("") diff --git a/khard/helpers/interactive.py b/khard/helpers/interactive.py index 5b5cfae..cd1dad5 100644 --- a/khard/helpers/interactive.py +++ b/khard/helpers/interactive.py @@ -1,13 +1,14 @@ """Helper functions for user interaction.""" +from __future__ import annotations + import contextlib from datetime import datetime from enum import Enum import os.path import subprocess from tempfile import NamedTemporaryFile -from typing import Callable, Generator, List, Optional, Sequence, \ - TypeVar, Union +from typing import Callable, Generator, Sequence, TypeVar from ..carddav_object import CarddavObject @@ -32,8 +33,8 @@ def confirm(message: str, accept_enter_key: bool = True) -> bool: "no" if accept_enter_key else None) -def ask(message: str, choices: List[str], default: Optional[str] = None, - help: Optional[str] = None) -> str: +def ask(message: str, choices: list[str], default: str | None = None, + help: str | None = None) -> str: """Ask the user to select one of the given choices :param message: a text to show to the user @@ -80,7 +81,7 @@ def ask(message: str, choices: List[str], default: Optional[str] = None, print(help) -def select(items: Sequence[T], include_none: bool = False) -> Optional[T]: +def select(items: Sequence[T], include_none: bool = False) -> T | None: """Ask the user to select an item from a list. The list should be displayed to the user before calling this function and @@ -120,8 +121,8 @@ class Editor: """Wrapper around subprocess.Popen to edit and merge files.""" - def __init__(self, editor: Union[str, List[str]], - merge_editor: Union[str, List[str]]) -> None: + def __init__(self, editor: str | list[str], + merge_editor: str | list[str]) -> None: self.editor = [editor] if isinstance(editor, str) else editor self.merge_editor = [merge_editor] if isinstance(merge_editor, str) \ else merge_editor @@ -143,7 +144,7 @@ def write_temp_file(text: str = "") -> Generator[str, None, None]: def _mtime(filename: str) -> datetime: return datetime.fromtimestamp(os.path.getmtime(filename)) - def edit_files(self, file1: str, file2: Optional[str] = None) -> EditState: + def edit_files(self, file1: str, file2: str | None = None) -> EditState: """Edit the given files If only one file is given the timestamp of this file is checked, if two @@ -169,8 +170,8 @@ def edit_files(self, file1: str, file2: Optional[str] = None) -> EditState: return EditState.modified def edit_templates(self, yaml2card: Callable[[str], CarddavObject], - template1: str, template2: Optional[str] = None - ) -> Optional[CarddavObject]: + template1: str, template2: str | None = None + ) -> CarddavObject | None: """Edit YAML templates of contacts and parse them back :param yaml2card: a function to convert the modified YAML templates diff --git a/khard/helpers/typing.py b/khard/helpers/typing.py index 1057cbd..4db56a9 100644 --- a/khard/helpers/typing.py +++ b/khard/helpers/typing.py @@ -1,8 +1,9 @@ """Helper code for type annotations and runtime type conversion.""" +from __future__ import annotations + from datetime import datetime from enum import Enum -from typing import Dict, List, Union class ObjectType(Enum): @@ -12,9 +13,9 @@ class ObjectType(Enum): # some type aliases -Date = Union[str, datetime] -StrList = Union[str, List[str]] -PostAddress = Dict[str, str] +Date = str | datetime +StrList = str | list[str] +PostAddress = dict[str, str] def convert_to_vcard(name: str, value: StrList, constraint: ObjectType @@ -44,7 +45,7 @@ def convert_to_vcard(name: str, value: StrList, constraint: ObjectType raise ValueError(f"{name} must be a string or a list with strings.") -def list_to_string(input: Union[str, List], delimiter: str) -> str: +def list_to_string(input: str | list, delimiter: str) -> str: """converts list to string recursively so that nested lists are supported :param input: a list of strings and lists of strings (and so on recursive) @@ -57,7 +58,7 @@ def list_to_string(input: Union[str, List], delimiter: str) -> str: return input -def string_to_list(input: Union[str, List[str]], delimiter: str) -> List[str]: +def string_to_list(input: str | list[str], delimiter: str) -> list[str]: if isinstance(input, list): return input return [x.strip() for x in input.split(delimiter)] diff --git a/khard/khard.py b/khard/khard.py index 2e65c61..48237f0 100644 --- a/khard/khard.py +++ b/khard/khard.py @@ -1,5 +1,7 @@ """Main application logic of khard including command line handling""" +from __future__ import annotations + from argparse import Namespace import datetime from email import message_from_string @@ -10,7 +12,7 @@ import os import sys import textwrap -from typing import cast, Callable, Dict, Iterable, List, Optional, Union +from typing import cast, Callable, Iterable from unidecode import unidecode @@ -152,17 +154,17 @@ def copy_contact(contact: CarddavObject, target_address_book: VdirAddressBook, contact.address_book, target_address_book)) -def list_address_books(address_books: Union[AddressBookCollection, - List[VdirAddressBook]]) -> None: +def list_address_books(address_books: AddressBookCollection | + list[VdirAddressBook]) -> None: table = [["Index", "Address book"]] for index, address_book in enumerate(address_books, 1): table.append([cast(str, index), address_book.name]) print(helpers.pretty_print(table)) -def list_contacts(vcard_list: List[CarddavObject], fields: Iterable[str] = (), +def list_contacts(vcard_list: list[CarddavObject], fields: Iterable[str] = (), parsable: bool = False) -> None: - selected_address_books: List[VdirAddressBook] = [] + selected_address_books: list[VdirAddressBook] = [] selected_kinds = set() for contact in vcard_list: if contact.address_book not in selected_address_books: @@ -224,16 +226,16 @@ def list_contacts(vcard_list: List[CarddavObject], fields: Iterable[str] = (), print(helpers.pretty_print(table)) -def list_with_headers(the_list: List[str], *headers: str) -> None: +def list_with_headers(the_list: list[str], *headers: str) -> None: table = [list(headers)] for row in the_list: table.append(row.split("\t")) print(helpers.pretty_print(table)) -def choose_address_book_from_list(header: str, abooks: Union[ - AddressBookCollection, List[VdirAddressBook]] - ) -> Optional[VdirAddressBook]: +def choose_address_book_from_list(header: str, abooks: + AddressBookCollection | list[VdirAddressBook] + ) -> VdirAddressBook | None: """Let the user select one of the given address books :param header: some text to print in front of the list @@ -250,9 +252,9 @@ def choose_address_book_from_list(header: str, abooks: Union[ return interactive.select(abooks) -def choose_vcard_from_list(header: str, vcards: List[CarddavObject], +def choose_vcard_from_list(header: str, vcards: list[CarddavObject], include_none: bool = False - ) -> Optional[CarddavObject]: + ) -> CarddavObject | None: """Let the user select a contact from a list :param header: some text to print in front of the list @@ -269,9 +271,8 @@ def choose_vcard_from_list(header: str, vcards: List[CarddavObject], return interactive.select(vcards, True) -def get_contact_list(address_books: Union[VdirAddressBook, - AddressBookCollection], - query: Query) -> List[CarddavObject]: +def get_contact_list(address_books: VdirAddressBook | AddressBookCollection, + query: Query) -> list[CarddavObject]: """Find contacts in the given address book grouped, sorted and reversed according to the loaded configuration. @@ -285,7 +286,7 @@ def get_contact_list(address_books: Union[VdirAddressBook, def sort_contacts(contacts: Iterable[CarddavObject], reverse: bool = False, - group: bool = False, sort: str = "first_name") -> List[ + group: bool = False, sort: str = "first_name") -> list[ CarddavObject]: """Sort a list of contacts @@ -296,7 +297,7 @@ def sort_contacts(contacts: Iterable[CarddavObject], reverse: bool = False, "last_name", "formatted_name" :returns: sorted contact list """ - keys: List[Callable] = [] + keys: list[Callable] = [] if group: keys.append(operator.attrgetter("address_book.name")) if sort == "first_name": @@ -312,7 +313,7 @@ def sort_contacts(contacts: Iterable[CarddavObject], reverse: bool = False, key=lambda x: [unidecode(key(x)).lower() for key in keys]) -def prepare_search_queries(args: Namespace) -> Dict[str, Query]: +def prepare_search_queries(args: Namespace) -> dict[str, Query]: """Prepare the search query string from the given command line args. Each address book can get a search query string to filter vCards before @@ -323,8 +324,8 @@ def prepare_search_queries(args: Namespace) -> Dict[str, Query]: :returns: a dict mapping abook names to their loading queries """ # get all possible search queries for address book parsing - source_queries: List[Query] = [] - target_queries: List[Query] = [] + source_queries: list[Query] = [] + target_queries: list[Query] = [] if "source_search_terms" in args: source_queries.append(args.source_search_terms) if "search_terms" in args: @@ -338,20 +339,20 @@ def prepare_search_queries(args: Namespace) -> Dict[str, Query]: # Get all possible search queries for address book parsing, always # depending on the fact if the address book is used to find source or # target contacts or both. - queries: Dict[str, List[Query]] = { + queries: dict[str, list[Query]] = { abook.name: [] for abook in config.abooks} for name in queries: if "addressbook" in args and name in args.addressbook: queries[name].append(source_query) if "target_addressbook" in args and name in args.target_addressbook: queries[name].append(target_query) - queries2: Dict[str, Query] = { + queries2: dict[str, Query] = { n: OrQuery.reduce(q) for n, q in queries.items()} logger.debug('Created query: %s', queries) return queries2 -def generate_contact_list(args: Namespace) -> List[CarddavObject]: +def generate_contact_list(args: Namespace) -> list[CarddavObject]: """Find the contact list with which we will work later on :param args: the command line arguments @@ -647,7 +648,7 @@ def add_email_to_contact(name: str, email_address: str, print("Done.\n\n{}".format(selected_vcard.pretty())) -def find_email_addresses(text: str, fields: List[str]) -> List[Address]: +def find_email_addresses(text: str, fields: list[str]) -> list[Address]: """Search the text for email addresses in the given fields. :param text: the text to search for email addresses @@ -656,7 +657,7 @@ def find_email_addresses(text: str, fields: List[str]) -> List[Address]: """ message = message_from_string(text, policy=SMTP_POLICY) - def extract_addresses(header) -> List[Address]: + def extract_addresses(header) -> list[Address]: if header and isinstance(header, (AddressHeader, Group)): return list(header.addresses) return [] @@ -677,7 +678,7 @@ def extract_addresses(header) -> List[Address]: def add_email_subcommand( text: str, abooks: AddressBookCollection, - fields: List[str], + fields: list[str], skip_already_added: bool) -> None: """Add a new email address to contacts, creating new contacts if necessary. @@ -704,7 +705,7 @@ def add_email_subcommand( print("No more email addresses") -def birthdays_subcommand(vcard_list: List[CarddavObject], parsable: bool +def birthdays_subcommand(vcard_list: list[CarddavObject], parsable: bool ) -> None: """Print birthday contact table. @@ -721,7 +722,7 @@ def birthdays_subcommand(vcard_list: List[CarddavObject], parsable: bool if isinstance(x.birthday, datetime.datetime) else (0, 0, x.birthday)) # add to string list - birthday_list: List[str] = [] + birthday_list: list[str] = [] formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) @@ -730,7 +731,7 @@ def birthdays_subcommand(vcard_list: List[CarddavObject], parsable: bool if parsable: # We did filter out None above but the type checker does not know # this. - bday = cast(Union[str, datetime.datetime], vcard.birthday) + bday = cast(str | datetime.datetime, vcard.birthday) if isinstance(bday, str): date = bday else: @@ -750,7 +751,7 @@ def birthdays_subcommand(vcard_list: List[CarddavObject], parsable: bool sys.exit(1) -def phone_subcommand(search_terms: Query, vcard_list: List[CarddavObject], +def phone_subcommand(search_terms: Query, vcard_list: list[CarddavObject], parsable: bool) -> None: """Print a phone application friendly contact table. @@ -763,7 +764,7 @@ def phone_subcommand(search_terms: Query, vcard_list: List[CarddavObject], formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) - numbers: List[str] = [] + numbers: list[str] = [] for vcard in vcard_list: field_line_list = [] for type, number_list in sorted(vcard.phone_numbers.items(), @@ -791,7 +792,7 @@ def phone_subcommand(search_terms: Query, vcard_list: List[CarddavObject], def post_address_subcommand(search_terms: Query, - vcard_list: List[CarddavObject], parsable: bool + vcard_list: list[CarddavObject], parsable: bool ) -> None: """Print a contact table with all postal / mailing addresses @@ -804,7 +805,7 @@ def post_address_subcommand(search_terms: Query, formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) - addresses: List[str] = [] + addresses: list[str] = [] for vcard in vcard_list: name = formatter.get_special_field(vcard, "name") # create post address line list @@ -835,7 +836,7 @@ def post_address_subcommand(search_terms: Query, sys.exit(1) -def email_subcommand(search_terms: Query, vcard_list: List[CarddavObject], +def email_subcommand(search_terms: Query, vcard_list: list[CarddavObject], parsable: bool, remove_first_line: bool) -> None: """Print a mail client friendly contacts table that is compatible with the default format used by mutt. @@ -858,7 +859,7 @@ def email_subcommand(search_terms: Query, vcard_list: List[CarddavObject], formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) - emails: List[str] = [] + emails: list[str] = [] for vcard in vcard_list: field_line_list = [] for type, email_list in sorted(vcard.emails.items(), @@ -891,7 +892,7 @@ def email_subcommand(search_terms: Query, vcard_list: List[CarddavObject], def _filter_email_post_or_phone_number_results(search_terms: Query, - field_line_list: List[str]) -> List[str]: + field_line_list: list[str]) -> list[str]: """Filter the created output of phone_subcommand, post_address_subcommand and email_subcommand by the given search term again. If no match is found, return the complete input list @@ -907,8 +908,8 @@ def _filter_email_post_or_phone_number_results(search_terms: Query, return matched_line_list if matched_line_list else field_line_list -def list_subcommand(vcard_list: List[CarddavObject], parsable: bool, - fields: List[str]) -> None: +def list_subcommand(vcard_list: list[CarddavObject], parsable: bool, + fields: list[str]) -> None: """Print a user friendly contacts table. :param vcard_list: the vCards to print @@ -984,7 +985,7 @@ def remove_subcommand(selected_vcard: CarddavObject, force: bool) -> None: selected_vcard.formatted_name)) -def merge_subcommand(vcards: List[CarddavObject], +def merge_subcommand(vcards: list[CarddavObject], abooks: AddressBookCollection, search_terms: Query ) -> None: """Merge two contacts into one. @@ -1019,7 +1020,7 @@ def merge_subcommand(vcards: List[CarddavObject], merge_existing_contacts(source_vcard, target_vcard, True) -def copy_or_move_subcommand(action: str, vcards: List[CarddavObject], +def copy_or_move_subcommand(action: str, vcards: list[CarddavObject], target_address_books: AddressBookCollection ) -> None: """Copy or move a contact to a different address book. @@ -1094,7 +1095,7 @@ def copy_or_move_subcommand(action: str, vcards: List[CarddavObject], break -def main(argv: List[str] = sys.argv[1:]) -> None: +def main(argv: list[str] = sys.argv[1:]) -> None: args, conf = cli.init(argv) # store the config instance in the module level variable diff --git a/khard/query.py b/khard/query.py index 7c39855..1222ed6 100644 --- a/khard/query.py +++ b/khard/query.py @@ -1,11 +1,13 @@ """Queries to match against contacts""" +from __future__ import annotations + import abc from datetime import datetime from functools import reduce from operator import and_, or_ import re -from typing import cast, Any, Dict, List, Optional, Union +from typing import cast, Any from . import carddav_object @@ -18,11 +20,11 @@ class Query(metaclass=abc.ABCMeta): """A query to match against strings, lists of strings and CarddavObjects""" @abc.abstractmethod - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: """Match the self query against the given thing""" @abc.abstractmethod - def get_term(self) -> Optional[str]: + def get_term(self) -> str | None: """Extract the search terms from a query.""" def __and__(self, other: "Query") -> "Query": @@ -70,7 +72,7 @@ class NullQuery(Query): """The null-query, it matches nothing.""" - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: return False def get_term(self) -> None: @@ -84,7 +86,7 @@ class AnyQuery(Query): """The match-anything-query, it always matches.""" - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: return True def get_term(self) -> str: @@ -104,7 +106,7 @@ class TermQuery(Query): def __init__(self, term: str) -> None: self._term = term.lower() - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: if isinstance(thing, str): return self._term in thing.lower() return self._term in thing.pretty().lower() @@ -130,14 +132,14 @@ def __init__(self, field: str, value: str) -> None: self._field = field super().__init__(value) - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: if isinstance(thing, str): return super().match(thing) if hasattr(thing, self._field): return self._match_union(getattr(thing, self._field)) return False - def _match_union(self, value: Union[str, datetime, List, Dict[str, Any]] + def _match_union(self, value: str | datetime | list | dict[str, Any] ) -> bool: if isinstance(value, str): return self.match(value) @@ -172,14 +174,14 @@ class AndQuery(Query): def __init__(self, first: Query, second: Query, *queries: Query) -> None: self._queries = (first, second, *queries) - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: return all(q.match(thing) for q in self._queries) - def get_term(self) -> Optional[str]: + def get_term(self) -> str | None: terms = [x.get_term() for x in self._queries] if None in terms: return None - return "".join(cast(List[str], terms)) + return "".join(cast(list[str], terms)) def __eq__(self, other: object) -> bool: return isinstance(other, AndQuery) \ @@ -189,7 +191,7 @@ def __hash__(self) -> int: return hash((AndQuery, frozenset(self._queries))) @staticmethod - def reduce(queries: List[Query], start: Optional[Query] = None) -> Query: + def reduce(queries: list[Query], start: Query | None = None) -> Query: return reduce(and_, queries, start or AnyQuery()) def __str__(self) -> str: @@ -203,10 +205,10 @@ class OrQuery(Query): def __init__(self, first: Query, second: Query, *queries: Query) -> None: self._queries = (first, second, *queries) - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: return any(q.match(thing) for q in self._queries) - def get_term(self) -> Optional[str]: + def get_term(self) -> str | None: terms = [x.get_term() for x in self._queries] if all(t is None for t in terms): return None @@ -220,7 +222,7 @@ def __hash__(self) -> int: return hash((OrQuery, frozenset(self._queries))) @staticmethod - def reduce(queries: List[Query], start: Optional[Query] = None) -> Query: + def reduce(queries: list[Query], start: Query | None = None) -> Query: return reduce(or_, queries, start or NullQuery()) def __str__(self) -> str: @@ -236,7 +238,7 @@ def __init__(self, term: str) -> None: self._props_query = OrQuery(FieldQuery("formatted_name", term), FieldQuery("nicknames", term)) - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: m = super().match if isinstance(thing, str): return m(thing) @@ -266,13 +268,13 @@ def __init__(self, value: str) -> None: super().__init__(FIELD_PHONE_NUMBERS, value) self._term_only_digits = self._strip_phone_number(value) - def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: + def match(self, thing: "str | carddav_object.CarddavObject") -> bool: if isinstance(thing, str): return self._match_union(thing) else: return super().match(thing) - def _match_union(self, value: Union[str, datetime, List, Dict[str, Any]] + def _match_union(self, value: str | datetime | list | dict[str, Any] ) -> bool: if isinstance(value, str): if self._term in value.lower() \ @@ -329,7 +331,7 @@ def __str__(self) -> str: return 'phone numbers:{}'.format(self._term) -def parse(string: str) -> Union[TermQuery, FieldQuery]: +def parse(string: str) -> TermQuery | FieldQuery: """Parse a string into a query object The input string interpreted as a :py:class:`FieldQuery` if it starts with diff --git a/test/test_vcard_wrapper.py b/test/test_vcard_wrapper.py index 3d622da..b2d8d9c 100644 --- a/test/test_vcard_wrapper.py +++ b/test/test_vcard_wrapper.py @@ -1,10 +1,11 @@ """Tests for the VCardWrapper class from the carddav module.""" # pylint: disable=missing-docstring +from __future__ import annotations + import contextlib import datetime import unittest -from typing import Union, List, Dict import vobject @@ -362,7 +363,7 @@ def _test_list_of_strings_as(self, key: str) -> None: wrapper = TestVCardWrapper() components = ('box', 'extended', 'street', 'code', 'city', 'region', 'country') - expected: Dict[str, Union[str, List[str]]] = {item: item + expected: dict[str, str | list[str]] = {item: item for item in components} expected[key] = ["a", "b"] index = components.index(key)