From 0d62069b67c308ef078f8d3d811e07e8e380d8e4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 13 Nov 2021 23:03:35 +0000 Subject: [PATCH] python: add mypy support and some type hints `deltachat.const` module now defines `__getattr__` and `__dir__` as suggested by https://www.python.org/dev/peps/pep-0562/ mypy detects that `__getattr__` is defined and does not show errors for `DC_*` constants which cannot be detected statically. mypy is added to `tox.ini`, so type check can be run with `tox -e mypy`. --- .github/workflows/ci.yml | 2 +- python/mypy.ini | 19 +++++ python/src/deltachat/__init__.py | 4 +- python/src/deltachat/account.py | 123 +++++++++++++++------------- python/src/deltachat/chat.py | 53 ++++++------ python/src/deltachat/const.py | 12 ++- python/src/deltachat/cutil.py | 12 ++- python/src/deltachat/direct_imap.py | 4 +- python/src/deltachat/events.py | 21 ++--- python/src/deltachat/provider.py | 2 +- python/src/deltachat/testplugin.py | 42 ++++++---- python/tox.ini | 10 +++ 12 files changed, 183 insertions(+), 121 deletions(-) create mode 100644 python/mypy.ini diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72f153082..d995051b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,4 +150,4 @@ jobs: DCC_RS_TARGET: debug DCC_RS_DEV: ${{ github.workspace }} working-directory: python - run: tox -e lint,doc,py3 + run: tox -e lint,mypy,doc,py3 diff --git a/python/mypy.ini b/python/mypy.ini new file mode 100644 index 000000000..7fbceefa1 --- /dev/null +++ b/python/mypy.ini @@ -0,0 +1,19 @@ +[mypy] + +[mypy-deltachat.capi.*] +ignore_missing_imports = True + +[mypy-pluggy.*] +ignore_missing_imports = True + +[mypy-cffi.*] +ignore_missing_imports = True + +[mypy-imapclient.*] +ignore_missing_imports = True + +[mypy-pytest.*] +ignore_missing_imports = True + +[mypy-_pytest.*] +ignore_missing_imports = True diff --git a/python/src/deltachat/__init__.py b/python/src/deltachat/__init__.py index dad8e121d..88a4566c0 100644 --- a/python/src/deltachat/__init__.py +++ b/python/src/deltachat/__init__.py @@ -19,9 +19,9 @@ except DistributionNotFound: def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}): if not _DC_EVENTNAME_MAP: - for name, val in vars(const).items(): + for name in dir(const): if name.startswith("DC_EVENT_"): - _DC_EVENTNAME_MAP[val] = name + _DC_EVENTNAME_MAP[getattr(const, name)] = name return _DC_EVENTNAME_MAP[integer] diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index cbda1412b..c20ed8b75 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -15,6 +15,7 @@ from .contact import Contact from .tracker import ImexTracker, ConfigureTracker from . import hookspec from .events import EventThread +from typing import Union, Any, Dict, Optional, List, Generator class MissingCredentials(ValueError): @@ -28,7 +29,7 @@ class Account(object): """ MissingCredentials = MissingCredentials - def __init__(self, db_path, os_name=None, logging=True): + def __init__(self, db_path, os_name=None, logging=True) -> None: """ initialize account object. :param db_path: a path to the account database. The database @@ -58,11 +59,11 @@ class Account(object): hook = hookspec.Global._get_plugin_manager().hook hook.dc_account_init(account=self) - def disable_logging(self): + def disable_logging(self) -> None: """ disable logging. """ self._logging = False - def enable_logging(self): + def enable_logging(self) -> None: """ re-enable logging. """ self._logging = True @@ -73,7 +74,7 @@ class Account(object): if self._logging: self._pm.hook.ac_log_line(message=msg) - def _check_config_key(self, name): + def _check_config_key(self, name: str) -> None: if name not in self._configkeys: raise KeyError("{!r} not a valid config key, existing keys: {!r}".format( name, self._configkeys)) @@ -105,19 +106,19 @@ class Account(object): cursor += len(entry) + 1 log("") - def set_stock_translation(self, id, string): + def set_stock_translation(self, id: int, string: str) -> None: """ set stock translation string. :param id: id of stock string (const.DC_STR_*) :param value: string to set as new transalation :returns: None """ - string = string.encode("utf8") - res = lib.dc_set_stock_translation(self._dc_context, id, string) + bytestring = string.encode("utf8") + res = lib.dc_set_stock_translation(self._dc_context, id, bytestring) if res == 0: raise ValueError("could not set translation string") - def set_config(self, name, value): + def set_config(self, name: str, value: Optional[str]) -> None: """ set configuration values. :param name: config key name (unicode) @@ -125,16 +126,16 @@ class Account(object): :returns: None """ self._check_config_key(name) - name = name.encode("utf8") - if name == b"addr" and self.is_configured(): + namebytes = name.encode("utf8") + if namebytes == b"addr" and self.is_configured(): raise ValueError("can not change 'addr' after account is configured.") if value is not None: - value = value.encode("utf8") + valuebytes = value.encode("utf8") else: - value = ffi.NULL - lib.dc_set_config(self._dc_context, name, value) + valuebytes = ffi.NULL + lib.dc_set_config(self._dc_context, namebytes, valuebytes) - def get_config(self, name): + def get_config(self, name: str): """ return unicode string value. :param name: configuration key to lookup (eg "addr" or "mail_pw") @@ -143,12 +144,12 @@ class Account(object): """ if name != "sys.config_keys": self._check_config_key(name) - name = name.encode("utf8") - res = lib.dc_get_config(self._dc_context, name) + namebytes = name.encode("utf8") + res = lib.dc_get_config(self._dc_context, namebytes) assert res != ffi.NULL, "config value not found for: {!r}".format(name) return from_dc_charpointer(res) - def _preconfigure_keypair(self, addr, public, secret): + def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None: """See dc_preconfigure_keypair() in deltachat.h. In other words, you don't need this. @@ -160,7 +161,7 @@ class Account(object): if res == 0: raise Exception("Failed to set key") - def update_config(self, kwargs): + def update_config(self, kwargs: Dict[str, Any]) -> None: """ update config values. :param kwargs: name=value config settings for this account. @@ -170,7 +171,7 @@ class Account(object): for key, value in kwargs.items(): self.set_config(key, str(value)) - def is_configured(self): + def is_configured(self) -> bool: """ determine if the account is configured already; an initial connection to SMTP/IMAP has been verified. @@ -178,7 +179,7 @@ class Account(object): """ return True if lib.dc_is_configured(self._dc_context) else False - def set_avatar(self, img_path): + def set_avatar(self, img_path: Optional[str]) -> None: """Set self avatar. :raises ValueError: if profile image could not be set @@ -190,12 +191,12 @@ class Account(object): assert os.path.exists(img_path), img_path self.set_config("selfavatar", img_path) - def check_is_configured(self): + def check_is_configured(self) -> None: """ Raise ValueError if this account is not configured. """ if not self.is_configured(): raise ValueError("need to configure first") - def get_latest_backupfile(self, backupdir): + def get_latest_backupfile(self, backupdir) -> Optional[str]: """ return the latest backup file in a given directory. """ res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir)) @@ -203,7 +204,7 @@ class Account(object): return None return from_dc_charpointer(res) - def get_blobdir(self): + def get_blobdir(self) -> Optional[str]: """ return the directory for files. All sent files are copied to this directory if necessary. @@ -211,15 +212,15 @@ class Account(object): """ return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context)) - def get_self_contact(self): + def get_self_contact(self) -> Contact: """ return this account's identity as a :class:`deltachat.contact.Contact`. :returns: :class:`deltachat.contact.Contact` """ return Contact(self, const.DC_CONTACT_ID_SELF) - def create_contact(self, obj, name=None): - """ create a (new) Contact or return an existing one. + def create_contact(self, obj, name: Optional[str] = None) -> Contact: + """create a (new) Contact or return an existing one. Calling this method will always result in the same underlying contact id. If there already is a Contact @@ -236,13 +237,13 @@ class Account(object): contact_id = lib.dc_create_contact(self._dc_context, name, addr) return Contact(self, contact_id) - def get_contact(self, obj): + def get_contact(self, obj) -> Optional[Contact]: if isinstance(obj, Contact): return obj (_, addr) = self.get_contact_addr_and_name(obj) return self.get_contact_by_addr(addr) - def get_contact_addr_and_name(self, obj, name=None): + def get_contact_addr_and_name(self, obj, name: Optional[str] = None): if isinstance(obj, Account): if not obj.is_configured(): raise ValueError("can only add addresses from configured accounts") @@ -260,7 +261,7 @@ class Account(object): name = displayname return (name, addr) - def delete_contact(self, contact): + def delete_contact(self, contact: Contact) -> bool: """ delete a Contact. :param contact: contact object obtained @@ -271,22 +272,23 @@ class Account(object): assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL return bool(lib.dc_delete_contact(self._dc_context, contact_id)) - def get_contact_by_addr(self, email): + def get_contact_by_addr(self, email: str) -> Optional[Contact]: """ get a contact for the email address or None if it's blocked or doesn't exist. """ _, addr = parseaddr(email) addr = as_dc_charpointer(addr) contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr) if contact_id: return self.get_contact_by_id(contact_id) + return None - def get_contact_by_id(self, contact_id): - """ return Contact instance or None. + def get_contact_by_id(self, contact_id: int) -> Contact: + """ return Contact instance or raise an exception. :param contact_id: integer id of this contact. - :returns: None or :class:`deltachat.contact.Contact` instance. + :returns: :class:`deltachat.contact.Contact` instance. """ return Contact(self, contact_id) - def get_blocked_contacts(self): + def get_blocked_contacts(self) -> List[Contact]: """ return a list of all blocked contacts. :returns: list of :class:`deltachat.contact.Contact` objects. @@ -297,8 +299,13 @@ class Account(object): ) return list(iter_array(dc_array, lambda x: Contact(self, x))) - def get_contacts(self, query=None, with_self=False, only_verified=False): - """ get a (filtered) list of contacts. + def get_contacts( + self, + query: Optional[str] = None, + with_self: bool = False, + only_verified: bool = False, + ) -> List[Contact]: + """get a (filtered) list of contacts. :param query: if a string is specified, only return contacts whose name or e-mail matches query. @@ -318,7 +325,7 @@ class Account(object): ) return list(iter_array(dc_array, lambda x: Contact(self, x))) - def get_fresh_messages(self): + def get_fresh_messages(self) -> Generator[Message, None, None]: """ yield all fresh messages from all chats. """ dc_array = ffi.gc( lib.dc_get_fresh_msgs(self._dc_context), @@ -326,12 +333,17 @@ class Account(object): ) yield from iter_array(dc_array, lambda x: Message.from_db(self, x)) - def create_chat(self, obj): + def create_chat(self, obj) -> Chat: """ Create a 1:1 chat with Account, Contact or e-mail address. """ return self.create_contact(obj).create_chat() - def create_group_chat(self, name, contacts=None, verified=False): - """ create a new group chat object. + def create_group_chat( + self, + name: str, + contacts: Optional[List[Contact]] = None, + verified: bool = False, + ) -> Chat: + """create a new group chat object. Chats are unpromoted until the first message is sent. @@ -347,7 +359,7 @@ class Account(object): chat.add_contact(contact) return chat - def get_chats(self): + def get_chats(self) -> List[Chat]: """ return list of chats. :returns: a list of :class:`deltachat.chat.Chat` objects. @@ -364,17 +376,17 @@ class Account(object): chatlist.append(Chat(self, chat_id)) return chatlist - def get_device_chat(self): + def get_device_chat(self) -> Chat: return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat() - def get_message_by_id(self, msg_id): + def get_message_by_id(self, msg_id: int) -> Message: """ return Message instance. :param msg_id: integer id of this message. :returns: :class:`deltachat.message.Message` instance. """ return Message.from_db(self, msg_id) - def get_chat_by_id(self, chat_id): + def get_chat_by_id(self, chat_id: int) -> Chat: """ return Chat instance. :param chat_id: integer id of this chat. :returns: :class:`deltachat.chat.Chat` instance. @@ -386,19 +398,18 @@ class Account(object): lib.dc_chat_unref(res) return Chat(self, chat_id) - def mark_seen_messages(self, messages): + def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None: """ mark the given set of messages as seen. :param messages: a list of message ids or Message instances. """ arr = array("i") for msg in messages: - msg = getattr(msg, "id", msg) - arr.append(msg) + arr.append(getattr(msg, "id", msg)) msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr)) lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages)) - def forward_messages(self, messages, chat): + def forward_messages(self, messages: List[Message], chat: Chat) -> None: """ Forward list of messages to a chat. :param messages: list of :class:`deltachat.message.Message` object. @@ -408,7 +419,7 @@ class Account(object): msg_ids = [msg.id for msg in messages] lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id) - def delete_messages(self, messages): + def delete_messages(self, messages: List[Message]) -> None: """ delete messages (local and remote). :param messages: list of :class:`deltachat.message.Message` object. @@ -477,7 +488,7 @@ class Account(object): raise RuntimeError("could not send out autocrypt setup message") return from_dc_charpointer(res) - def get_setup_contact_qr(self): + def get_setup_contact_qr(self) -> Optional[str]: """ get/create Setup-Contact QR Code as ascii-string. this string needs to be transferred to another DC account @@ -527,7 +538,9 @@ class Account(object): raise ValueError("could not join group") return Chat(self, chat_id) - def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0): + def set_location( + self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0 + ) -> None: """set a new location. It effects all chats where we currently have enabled location streaming. @@ -621,7 +634,7 @@ class Account(object): """ lib.dc_maybe_network(self._dc_context) - def configure(self, reconfigure=False): + def configure(self, reconfigure: bool = False) -> ConfigureTracker: """ Start configuration process and return a Configtracker instance on which you can block with wait_finish() to get a True/False success value for the configuration process. @@ -634,11 +647,11 @@ class Account(object): lib.dc_configure(self._dc_context) return configtracker - def wait_shutdown(self): + def wait_shutdown(self) -> None: """ wait until shutdown of this account has completed. """ self._shutdown_event.wait() - def stop_io(self): + def stop_io(self) -> None: """ stop core IO scheduler if it is running. """ self.log("stop_ongoing") self.stop_ongoing() @@ -646,7 +659,7 @@ class Account(object): self.log("dc_stop_io (stop core IO scheduler)") lib.dc_stop_io(self._dc_context) - def shutdown(self): + def shutdown(self) -> None: """ shutdown and destroy account (stop callback thread, close and remove underlying dc_context).""" if self._dc_context is None: diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index 70dfd4dcc..e76b1154b 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -9,6 +9,7 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array from .capi import lib, ffi from . import const from .message import Message +from typing import Optional class Chat(object): @@ -17,20 +18,20 @@ class Chat(object): You obtain instances of it through :class:`deltachat.account.Account`. """ - def __init__(self, account, id): + def __init__(self, account, id) -> None: from .account import Account assert isinstance(account, Account), repr(account) self.account = account self.id = id - def __eq__(self, other): + def __eq__(self, other) -> bool: return self.id == getattr(other, "id", None) and \ self.account._dc_context == other.account._dc_context - def __ne__(self, other): + def __ne__(self, other) -> bool: return not (self == other) - def __repr__(self): + def __repr__(self) -> str: return "".format(self.id, self.get_name()) @property @@ -40,7 +41,7 @@ class Chat(object): lib.dc_chat_unref ) - def delete(self): + def delete(self) -> None: """Delete this chat and all its messages. Note: @@ -50,24 +51,24 @@ class Chat(object): """ lib.dc_delete_chat(self.account._dc_context, self.id) - def block(self): + def block(self) -> None: """Block this chat.""" lib.dc_block_chat(self.account._dc_context, self.id) - def accept(self): + def accept(self) -> None: """Accept this contact request chat.""" lib.dc_accept_chat(self.account._dc_context, self.id) # ------ chat status/metadata API ------------------------------ - def is_group(self): + def is_group(self) -> bool: """ return true if this chat is a group chat. :returns: True if chat is a group-chat, false if it's a contact 1:1 chat. """ return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP - def is_muted(self): + def is_muted(self) -> bool: """ return true if this chat is muted. :returns: True if chat is muted, False otherwise. @@ -90,7 +91,7 @@ class Chat(object): """ return not lib.dc_chat_is_unpromoted(self._dc_chat) - def can_send(self): + def can_send(self) -> bool: """Check if messages can be sent to a give chat. This is not true eg. for the contact requests or for the device-talk @@ -98,30 +99,30 @@ class Chat(object): """ return lib.dc_chat_can_send(self._dc_chat) - def is_protected(self): + def is_protected(self) -> bool: """ return True if this chat is a protected chat. :returns: True if chat is protected, False otherwise. """ return lib.dc_chat_is_protected(self._dc_chat) - def get_name(self): + def get_name(self) -> Optional[str]: """ return name of this chat. :returns: unicode name """ return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat)) - def set_name(self, name): + def set_name(self, name: str) -> bool: """ set name of this chat. :param name: as a unicode string. - :returns: None + :returns: True on success, False otherwise """ name = as_dc_charpointer(name) - return lib.dc_set_chat_name(self.account._dc_context, self.id, name) + return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name)) - def mute(self, duration=None): + def mute(self, duration: Optional[int] = None) -> None: """ mutes the chat :param duration: Number of seconds to mute the chat for. None to mute until unmuted again. @@ -135,7 +136,7 @@ class Chat(object): if not bool(ret): raise ValueError("Call to dc_set_chat_mute_duration failed") - def unmute(self): + def unmute(self) -> None: """ unmutes the chat :returns: None @@ -144,7 +145,7 @@ class Chat(object): if not bool(ret): raise ValueError("Failed to unmute chat") - def get_mute_duration(self): + def get_mute_duration(self) -> int: """ Returns the number of seconds until the mute of this chat is lifted. :param duration: @@ -152,37 +153,37 @@ class Chat(object): """ return lib.dc_chat_get_remaining_mute_duration(self._dc_chat) - def get_ephemeral_timer(self): + def get_ephemeral_timer(self) -> int: """ get ephemeral timer. :returns: ephemeral timer value in seconds """ return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id) - def set_ephemeral_timer(self, timer): + def set_ephemeral_timer(self, timer: int) -> bool: """ set ephemeral timer. :param: timer value in seconds - :returns: None + :returns: True on success, False otherwise """ - return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer) + return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)) - def get_type(self): + def get_type(self) -> int: """ (deprecated) return type of this chat. :returns: one of const.DC_CHAT_TYPE_* """ return lib.dc_chat_get_type(self._dc_chat) - def get_encryption_info(self): + def get_encryption_info(self) -> Optional[str]: """Return encryption info for this chat. :returns: a string with encryption preferences of all chat members""" res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id) return from_dc_charpointer(res) - def get_join_qr(self): + def get_join_qr(self) -> Optional[str]: """ get/create Join-Group QR Code as ascii-string. this string needs to be transferred to another DC account @@ -194,7 +195,7 @@ class Chat(object): # ------ chat messaging API ------------------------------ - def send_msg(self, msg): + def send_msg(self, msg: Message) -> Message: """send a message by using a ready Message object. :param msg: a :class:`deltachat.message.Message` instance diff --git a/python/src/deltachat/const.py b/python/src/deltachat/const.py index 0d86d6344..4dc49500e 100644 --- a/python/src/deltachat/const.py +++ b/python/src/deltachat/const.py @@ -1,7 +1,13 @@ +from typing import Any, List + from .capi import lib -for name in dir(lib): +def __getattr__(name: str) -> Any: if name.startswith("DC_"): - globals()[name] = getattr(lib, name) -del name + return getattr(lib, name) + return globals()[name] + + +def __dir__() -> List[str]: + return sorted(name for name in dir(lib) if name.startswith("DC_")) diff --git a/python/src/deltachat/cutil.py b/python/src/deltachat/cutil.py index 4ef53e036..da98212a5 100644 --- a/python/src/deltachat/cutil.py +++ b/python/src/deltachat/cutil.py @@ -1,6 +1,9 @@ from .capi import lib from .capi import ffi from datetime import datetime, timezone +from typing import Optional, TypeVar, Generator, Callable + +T = TypeVar('T') def as_dc_charpointer(obj): @@ -11,21 +14,22 @@ def as_dc_charpointer(obj): return obj -def iter_array(dc_array_t, constructor): +def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]: for i in range(0, lib.dc_array_get_cnt(dc_array_t)): yield constructor(lib.dc_array_get_id(dc_array_t, i)) -def from_dc_charpointer(obj): +def from_dc_charpointer(obj) -> Optional[str]: if obj != ffi.NULL: return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8") + return None class DCLot: - def __init__(self, dc_lot): + def __init__(self, dc_lot) -> None: self._dc_lot = dc_lot - def id(self): + def id(self) -> int: return lib.dc_lot_get_id(self._dc_lot) def state(self): diff --git a/python/src/deltachat/direct_imap.py b/python/src/deltachat/direct_imap.py index d5291701c..74b967401 100644 --- a/python/src/deltachat/direct_imap.py +++ b/python/src/deltachat/direct_imap.py @@ -11,7 +11,7 @@ from imapclient import IMAPClient from imapclient.exceptions import IMAPClientError import imaplib import deltachat -from deltachat import const +from deltachat import const, Account SEEN = b'\\Seen' @@ -62,7 +62,7 @@ def dc_account_after_shutdown(account): class DirectImap: - def __init__(self, account): + def __init__(self, account: Account) -> None: self.account = account self.logid = account.get_config("displayname") or id(account) self._idling = False diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py index 2b1445244..8fe60a261 100644 --- a/python/src/deltachat/events.py +++ b/python/src/deltachat/events.py @@ -13,7 +13,7 @@ from .cutil import from_dc_charpointer class FFIEvent: - def __init__(self, name, data1, data2): + def __init__(self, name: str, data1, data2): self.name = name self.data1 = data1 self.data2 = data2 @@ -29,13 +29,13 @@ class FFIEventLogger: # to prevent garbled logging _loglock = threading.RLock() - def __init__(self, account): + def __init__(self, account) -> None: self.account = account self.logid = self.account.get_config("displayname") self.init_time = time.time() @account_hookimpl - def ac_process_ffi_event(self, ffi_event): + def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None: self.account.log(str(ffi_event)) @account_hookimpl @@ -69,7 +69,7 @@ class FFIEventTracker: self._event_queue = Queue() @account_hookimpl - def ac_process_ffi_event(self, ffi_event): + def ac_process_ffi_event(self, ffi_event: FFIEvent): self._event_queue.put(ffi_event) def set_timeout(self, timeout): @@ -96,7 +96,7 @@ class FFIEventTracker: if rex.match(ev.name): return ev - def get_info_contains(self, regex): + def get_info_contains(self, regex: str) -> FFIEvent: rex = re.compile(regex) while 1: ev = self.get_matching("DC_EVENT_INFO") @@ -176,6 +176,7 @@ class FFIEventTracker: ev = self.get_matching("DC_EVENT_MSGS_CHANGED") if ev.data2 > 0: return self.account.get_message_by_id(ev.data2) + return None def wait_msg_delivered(self, msg): ev = self.get_matching("DC_EVENT_MSG_DELIVERED") @@ -189,7 +190,7 @@ class EventThread(threading.Thread): With each Account init this callback thread is started. """ - def __init__(self, account): + def __init__(self, account) -> None: self.account = account super(EventThread, self).__init__(name="events") self.setDaemon(True) @@ -202,17 +203,17 @@ class EventThread(threading.Thread): yield self.account.log(message + " FINISHED") - def mark_shutdown(self): + def mark_shutdown(self) -> None: self._marked_for_shutdown = True - def wait(self, timeout=None): + def wait(self, timeout=None) -> None: if self == threading.current_thread(): # we are in the callback thread and thus cannot # wait for the thread-loop to finish. return self.join(timeout=timeout) - def run(self): + def run(self) -> None: """ get and run events until shutdown. """ with self.log_execution("EVENT THREAD"): self._inner_run() @@ -250,7 +251,7 @@ class EventThread(threading.Thread): if self.account._dc_context is not None: raise - def _map_ffi_event(self, ffi_event): + def _map_ffi_event(self, ffi_event: FFIEvent): name = ffi_event.name account = self.account if name == "DC_EVENT_CONFIGURE_PROGRESS": diff --git a/python/src/deltachat/provider.py b/python/src/deltachat/provider.py index 487a84192..d4b7c7934 100644 --- a/python/src/deltachat/provider.py +++ b/python/src/deltachat/provider.py @@ -14,7 +14,7 @@ class Provider(object): :param domain: The email to get the provider info for. """ - def __init__(self, account, addr): + def __init__(self, account, addr) -> None: provider = ffi.gc( lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)), lib.dc_provider_unref, diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index eecc0133a..e172e3e32 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -9,6 +9,7 @@ import fnmatch import time import weakref import tempfile +from typing import List, Dict, Callable import pytest import requests @@ -126,7 +127,7 @@ def pytest_report_header(config, startdir): class SessionLiveConfigFromFile: - def __init__(self, fn): + def __init__(self, fn) -> None: self.fn = fn self.configlist = [] for line in open(fn): @@ -137,19 +138,21 @@ class SessionLiveConfigFromFile: d[name] = value self.configlist.append(d) - def get(self, index): + def get(self, index: int): return self.configlist[index] - def exists(self): + def exists(self) -> bool: return bool(self.configlist) class SessionLiveConfigFromURL: - def __init__(self, url): + configlist: List[Dict[str, str]] + + def __init__(self, url: str) -> None: self.configlist = [] self.url = url - def get(self, index): + def get(self, index: int): try: return self.configlist[index] except IndexError: @@ -162,7 +165,7 @@ class SessionLiveConfigFromURL: self.configlist.append(config) return config - def exists(self): + def exists(self) -> bool: return bool(self.configlist) @@ -179,7 +182,7 @@ def session_liveconfig(request): @pytest.fixture def data(request): class Data: - def __init__(self): + def __init__(self) -> None: # trying to find test data heuristically # because we are run from a dev-setup with pytest direct, # through tox, and then maybe also from deltachat-binding @@ -210,7 +213,10 @@ def data(request): def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): class AccountMaker: - def __init__(self): + _finalizers: List[Callable[[], None]] + _accounts: List[Account] + + def __init__(self) -> None: self.live_count = 0 self.offline_count = 0 self._finalizers = [] @@ -423,7 +429,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): pass imap.dump_imap_structures(tmpdir, logfile=logfile) - def get_accepted_chat(self, ac1, ac2): + def get_accepted_chat(self, ac1: Account, ac2: Account): ac2.create_chat(ac1) return ac1.create_chat(ac2) @@ -451,7 +457,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): class BotProcess: - def __init__(self, popen, bot_cfg): + stdout_queue: queue.Queue + + def __init__(self, popen, bot_cfg) -> None: self.popen = popen self.addr = bot_cfg["addr"] @@ -459,10 +467,10 @@ class BotProcess: # the (unicode) lines available for readers through a queue. self.stdout_queue = queue.Queue() self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread") - t.setDaemon(1) + t.setDaemon(True) t.start() - def _run_stdout_thread(self): + def _run_stdout_thread(self) -> None: try: while 1: line = self.popen.stdout.readline() @@ -474,10 +482,10 @@ class BotProcess: finally: self.stdout_queue.put(None) - def kill(self): + def kill(self) -> None: self.popen.kill() - def wait(self, timeout=30): + def wait(self, timeout=30) -> None: self.popen.wait(timeout=timeout) def fnmatch_lines(self, pattern_lines): @@ -509,14 +517,14 @@ def tmp_db_path(tmpdir): @pytest.fixture def lp(): class Printer: - def sec(self, msg): + def sec(self, msg: str) -> None: print() print("=" * 10, msg, "=" * 10) - def step(self, msg): + def step(self, msg: str) -> None: print("-" * 5, "step " + msg, "-" * 5) - def indent(self, msg): + def indent(self, msg: str) -> None: print(" " + msg) return Printer() diff --git a/python/tox.ini b/python/tox.ini index bb9a07b1a..a0814fef8 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -3,6 +3,7 @@ isolated_build = true envlist = py3 lint + mypy auditwheels [testenv] @@ -43,6 +44,15 @@ commands = flake8 tests/ examples/ rst-lint --encoding 'utf-8' README.rst +[testenv:mypy] +deps = + mypy + typing + types-setuptools + types-requests +commands = + mypy --no-incremental src/ + [testenv:doc] changedir=doc deps =