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`.
This commit is contained in:
link2xt
2021-11-13 23:03:35 +00:00
parent 56cf2e6596
commit 0d62069b67
12 changed files with 183 additions and 121 deletions

View File

@@ -150,4 +150,4 @@ jobs:
DCC_RS_TARGET: debug DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }} DCC_RS_DEV: ${{ github.workspace }}
working-directory: python working-directory: python
run: tox -e lint,doc,py3 run: tox -e lint,mypy,doc,py3

19
python/mypy.ini Normal file
View File

@@ -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

View File

@@ -19,9 +19,9 @@ except DistributionNotFound:
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}): def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _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_"): if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[val] = name _DC_EVENTNAME_MAP[getattr(const, name)] = name
return _DC_EVENTNAME_MAP[integer] return _DC_EVENTNAME_MAP[integer]

View File

@@ -15,6 +15,7 @@ from .contact import Contact
from .tracker import ImexTracker, ConfigureTracker from .tracker import ImexTracker, ConfigureTracker
from . import hookspec from . import hookspec
from .events import EventThread from .events import EventThread
from typing import Union, Any, Dict, Optional, List, Generator
class MissingCredentials(ValueError): class MissingCredentials(ValueError):
@@ -28,7 +29,7 @@ class Account(object):
""" """
MissingCredentials = MissingCredentials 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. """ initialize account object.
:param db_path: a path to the account database. The database :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 = hookspec.Global._get_plugin_manager().hook
hook.dc_account_init(account=self) hook.dc_account_init(account=self)
def disable_logging(self): def disable_logging(self) -> None:
""" disable logging. """ """ disable logging. """
self._logging = False self._logging = False
def enable_logging(self): def enable_logging(self) -> None:
""" re-enable logging. """ """ re-enable logging. """
self._logging = True self._logging = True
@@ -73,7 +74,7 @@ class Account(object):
if self._logging: if self._logging:
self._pm.hook.ac_log_line(message=msg) 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: if name not in self._configkeys:
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format( raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
name, self._configkeys)) name, self._configkeys))
@@ -105,19 +106,19 @@ class Account(object):
cursor += len(entry) + 1 cursor += len(entry) + 1
log("") log("")
def set_stock_translation(self, id, string): def set_stock_translation(self, id: int, string: str) -> None:
""" set stock translation string. """ set stock translation string.
:param id: id of stock string (const.DC_STR_*) :param id: id of stock string (const.DC_STR_*)
:param value: string to set as new transalation :param value: string to set as new transalation
:returns: None :returns: None
""" """
string = string.encode("utf8") bytestring = string.encode("utf8")
res = lib.dc_set_stock_translation(self._dc_context, id, string) res = lib.dc_set_stock_translation(self._dc_context, id, bytestring)
if res == 0: if res == 0:
raise ValueError("could not set translation string") 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. """ set configuration values.
:param name: config key name (unicode) :param name: config key name (unicode)
@@ -125,16 +126,16 @@ class Account(object):
:returns: None :returns: None
""" """
self._check_config_key(name) self._check_config_key(name)
name = name.encode("utf8") namebytes = name.encode("utf8")
if name == b"addr" and self.is_configured(): if namebytes == b"addr" and self.is_configured():
raise ValueError("can not change 'addr' after account is configured.") raise ValueError("can not change 'addr' after account is configured.")
if value is not None: if value is not None:
value = value.encode("utf8") valuebytes = value.encode("utf8")
else: else:
value = ffi.NULL valuebytes = ffi.NULL
lib.dc_set_config(self._dc_context, name, value) lib.dc_set_config(self._dc_context, namebytes, valuebytes)
def get_config(self, name): def get_config(self, name: str):
""" return unicode string value. """ return unicode string value.
:param name: configuration key to lookup (eg "addr" or "mail_pw") :param name: configuration key to lookup (eg "addr" or "mail_pw")
@@ -143,12 +144,12 @@ class Account(object):
""" """
if name != "sys.config_keys": if name != "sys.config_keys":
self._check_config_key(name) self._check_config_key(name)
name = name.encode("utf8") namebytes = name.encode("utf8")
res = lib.dc_get_config(self._dc_context, name) res = lib.dc_get_config(self._dc_context, namebytes)
assert res != ffi.NULL, "config value not found for: {!r}".format(name) assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res) 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. """See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this. In other words, you don't need this.
@@ -160,7 +161,7 @@ class Account(object):
if res == 0: if res == 0:
raise Exception("Failed to set key") raise Exception("Failed to set key")
def update_config(self, kwargs): def update_config(self, kwargs: Dict[str, Any]) -> None:
""" update config values. """ update config values.
:param kwargs: name=value config settings for this account. :param kwargs: name=value config settings for this account.
@@ -170,7 +171,7 @@ class Account(object):
for key, value in kwargs.items(): for key, value in kwargs.items():
self.set_config(key, str(value)) 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 """ determine if the account is configured already; an initial connection
to SMTP/IMAP has been verified. 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 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. """Set self avatar.
:raises ValueError: if profile image could not be set :raises ValueError: if profile image could not be set
@@ -190,12 +191,12 @@ class Account(object):
assert os.path.exists(img_path), img_path assert os.path.exists(img_path), img_path
self.set_config("selfavatar", 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. """ """ Raise ValueError if this account is not configured. """
if not self.is_configured(): if not self.is_configured():
raise ValueError("need to configure first") 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. """ return the latest backup file in a given directory.
""" """
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir)) res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
@@ -203,7 +204,7 @@ class Account(object):
return None return None
return from_dc_charpointer(res) return from_dc_charpointer(res)
def get_blobdir(self): def get_blobdir(self) -> Optional[str]:
""" return the directory for files. """ return the directory for files.
All sent files are copied to this directory if necessary. 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)) 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`. """ return this account's identity as a :class:`deltachat.contact.Contact`.
:returns: :class:`deltachat.contact.Contact` :returns: :class:`deltachat.contact.Contact`
""" """
return Contact(self, const.DC_CONTACT_ID_SELF) return Contact(self, const.DC_CONTACT_ID_SELF)
def create_contact(self, obj, name=None): def create_contact(self, obj, name: Optional[str] = None) -> Contact:
""" create a (new) Contact or return an existing one. """create a (new) Contact or return an existing one.
Calling this method will always result in the same Calling this method will always result in the same
underlying contact id. If there already is a Contact 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) contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id) return Contact(self, contact_id)
def get_contact(self, obj): def get_contact(self, obj) -> Optional[Contact]:
if isinstance(obj, Contact): if isinstance(obj, Contact):
return obj return obj
(_, addr) = self.get_contact_addr_and_name(obj) (_, addr) = self.get_contact_addr_and_name(obj)
return self.get_contact_by_addr(addr) 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 isinstance(obj, Account):
if not obj.is_configured(): if not obj.is_configured():
raise ValueError("can only add addresses from configured accounts") raise ValueError("can only add addresses from configured accounts")
@@ -260,7 +261,7 @@ class Account(object):
name = displayname name = displayname
return (name, addr) return (name, addr)
def delete_contact(self, contact): def delete_contact(self, contact: Contact) -> bool:
""" delete a Contact. """ delete a Contact.
:param contact: contact object obtained :param contact: contact object obtained
@@ -271,22 +272,23 @@ class Account(object):
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
return bool(lib.dc_delete_contact(self._dc_context, contact_id)) 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. """ """ get a contact for the email address or None if it's blocked or doesn't exist. """
_, addr = parseaddr(email) _, addr = parseaddr(email)
addr = as_dc_charpointer(addr) addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr) contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
if contact_id: if contact_id:
return self.get_contact_by_id(contact_id) return self.get_contact_by_id(contact_id)
return None
def get_contact_by_id(self, contact_id): def get_contact_by_id(self, contact_id: int) -> Contact:
""" return Contact instance or None. """ return Contact instance or raise an exception.
:param contact_id: integer id of this contact. :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) return Contact(self, contact_id)
def get_blocked_contacts(self): def get_blocked_contacts(self) -> List[Contact]:
""" return a list of all blocked contacts. """ return a list of all blocked contacts.
:returns: list of :class:`deltachat.contact.Contact` objects. :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))) return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts(self, query=None, with_self=False, only_verified=False): def get_contacts(
""" get a (filtered) list of 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 :param query: if a string is specified, only return contacts
whose name or e-mail matches query. 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))) 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. """ """ yield all fresh messages from all chats. """
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_fresh_msgs(self._dc_context), 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)) 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. """ """ Create a 1:1 chat with Account, Contact or e-mail address. """
return self.create_contact(obj).create_chat() return self.create_contact(obj).create_chat()
def create_group_chat(self, name, contacts=None, verified=False): def create_group_chat(
""" create a new group chat object. 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. Chats are unpromoted until the first message is sent.
@@ -347,7 +359,7 @@ class Account(object):
chat.add_contact(contact) chat.add_contact(contact)
return chat return chat
def get_chats(self): def get_chats(self) -> List[Chat]:
""" return list of chats. """ return list of chats.
:returns: a list of :class:`deltachat.chat.Chat` objects. :returns: a list of :class:`deltachat.chat.Chat` objects.
@@ -364,17 +376,17 @@ class Account(object):
chatlist.append(Chat(self, chat_id)) chatlist.append(Chat(self, chat_id))
return chatlist return chatlist
def get_device_chat(self): def get_device_chat(self) -> Chat:
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_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. """ return Message instance.
:param msg_id: integer id of this message. :param msg_id: integer id of this message.
:returns: :class:`deltachat.message.Message` instance. :returns: :class:`deltachat.message.Message` instance.
""" """
return Message.from_db(self, msg_id) 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. """ return Chat instance.
:param chat_id: integer id of this chat. :param chat_id: integer id of this chat.
:returns: :class:`deltachat.chat.Chat` instance. :returns: :class:`deltachat.chat.Chat` instance.
@@ -386,19 +398,18 @@ class Account(object):
lib.dc_chat_unref(res) lib.dc_chat_unref(res)
return Chat(self, chat_id) 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. """ mark the given set of messages as seen.
:param messages: a list of message ids or Message instances. :param messages: a list of message ids or Message instances.
""" """
arr = array("i") arr = array("i")
for msg in messages: for msg in messages:
msg = getattr(msg, "id", msg) arr.append(getattr(msg, "id", msg))
arr.append(msg)
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr)) msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages)) 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. """ Forward list of messages to a chat.
:param messages: list of :class:`deltachat.message.Message` object. :param messages: list of :class:`deltachat.message.Message` object.
@@ -408,7 +419,7 @@ class Account(object):
msg_ids = [msg.id for msg in messages] msg_ids = [msg.id for msg in messages]
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id) 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). """ delete messages (local and remote).
:param messages: list of :class:`deltachat.message.Message` object. :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") raise RuntimeError("could not send out autocrypt setup message")
return from_dc_charpointer(res) 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. """ get/create Setup-Contact QR Code as ascii-string.
this string needs to be transferred to another DC account this string needs to be transferred to another DC account
@@ -527,7 +538,9 @@ class Account(object):
raise ValueError("could not join group") raise ValueError("could not join group")
return Chat(self, chat_id) 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 """set a new location. It effects all chats where we currently
have enabled location streaming. have enabled location streaming.
@@ -621,7 +634,7 @@ class Account(object):
""" """
lib.dc_maybe_network(self._dc_context) 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 """ Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success on which you can block with wait_finish() to get a True/False success
value for the configuration process. value for the configuration process.
@@ -634,11 +647,11 @@ class Account(object):
lib.dc_configure(self._dc_context) lib.dc_configure(self._dc_context)
return configtracker return configtracker
def wait_shutdown(self): def wait_shutdown(self) -> None:
""" wait until shutdown of this account has completed. """ """ wait until shutdown of this account has completed. """
self._shutdown_event.wait() self._shutdown_event.wait()
def stop_io(self): def stop_io(self) -> None:
""" stop core IO scheduler if it is running. """ """ stop core IO scheduler if it is running. """
self.log("stop_ongoing") self.log("stop_ongoing")
self.stop_ongoing() self.stop_ongoing()
@@ -646,7 +659,7 @@ class Account(object):
self.log("dc_stop_io (stop core IO scheduler)") self.log("dc_stop_io (stop core IO scheduler)")
lib.dc_stop_io(self._dc_context) lib.dc_stop_io(self._dc_context)
def shutdown(self): def shutdown(self) -> None:
""" shutdown and destroy account (stop callback thread, close and remove """ shutdown and destroy account (stop callback thread, close and remove
underlying dc_context).""" underlying dc_context)."""
if self._dc_context is None: if self._dc_context is None:

View File

@@ -9,6 +9,7 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi from .capi import lib, ffi
from . import const from . import const
from .message import Message from .message import Message
from typing import Optional
class Chat(object): class Chat(object):
@@ -17,20 +18,20 @@ class Chat(object):
You obtain instances of it through :class:`deltachat.account.Account`. 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 from .account import Account
assert isinstance(account, Account), repr(account) assert isinstance(account, Account), repr(account)
self.account = account self.account = account
self.id = id self.id = id
def __eq__(self, other): def __eq__(self, other) -> bool:
return self.id == getattr(other, "id", None) and \ return self.id == getattr(other, "id", None) and \
self.account._dc_context == other.account._dc_context self.account._dc_context == other.account._dc_context
def __ne__(self, other): def __ne__(self, other) -> bool:
return not (self == other) return not (self == other)
def __repr__(self): def __repr__(self) -> str:
return "<Chat id={} name={}>".format(self.id, self.get_name()) return "<Chat id={} name={}>".format(self.id, self.get_name())
@property @property
@@ -40,7 +41,7 @@ class Chat(object):
lib.dc_chat_unref lib.dc_chat_unref
) )
def delete(self): def delete(self) -> None:
"""Delete this chat and all its messages. """Delete this chat and all its messages.
Note: Note:
@@ -50,24 +51,24 @@ class Chat(object):
""" """
lib.dc_delete_chat(self.account._dc_context, self.id) lib.dc_delete_chat(self.account._dc_context, self.id)
def block(self): def block(self) -> None:
"""Block this chat.""" """Block this chat."""
lib.dc_block_chat(self.account._dc_context, self.id) lib.dc_block_chat(self.account._dc_context, self.id)
def accept(self): def accept(self) -> None:
"""Accept this contact request chat.""" """Accept this contact request chat."""
lib.dc_accept_chat(self.account._dc_context, self.id) lib.dc_accept_chat(self.account._dc_context, self.id)
# ------ chat status/metadata API ------------------------------ # ------ chat status/metadata API ------------------------------
def is_group(self): def is_group(self) -> bool:
""" return true if this chat is a group chat. """ 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. :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 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. """ return true if this chat is muted.
:returns: True if chat is muted, False otherwise. :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) 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. """Check if messages can be sent to a give chat.
This is not true eg. for the contact requests or for the device-talk 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) 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. """ return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise. :returns: True if chat is protected, False otherwise.
""" """
return lib.dc_chat_is_protected(self._dc_chat) return lib.dc_chat_is_protected(self._dc_chat)
def get_name(self): def get_name(self) -> Optional[str]:
""" return name of this chat. """ return name of this chat.
:returns: unicode name :returns: unicode name
""" """
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat)) 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. """ set name of this chat.
:param name: as a unicode string. :param name: as a unicode string.
:returns: None :returns: True on success, False otherwise
""" """
name = as_dc_charpointer(name) 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 """ mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again. :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): if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed") raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self): def unmute(self) -> None:
""" unmutes the chat """ unmutes the chat
:returns: None :returns: None
@@ -144,7 +145,7 @@ class Chat(object):
if not bool(ret): if not bool(ret):
raise ValueError("Failed to unmute chat") 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. """ Returns the number of seconds until the mute of this chat is lifted.
:param duration: :param duration:
@@ -152,37 +153,37 @@ class Chat(object):
""" """
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat) 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. """ get ephemeral timer.
:returns: ephemeral timer value in seconds :returns: ephemeral timer value in seconds
""" """
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id) 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. """ set ephemeral timer.
:param: timer value in seconds :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. """ (deprecated) return type of this chat.
:returns: one of const.DC_CHAT_TYPE_* :returns: one of const.DC_CHAT_TYPE_*
""" """
return lib.dc_chat_get_type(self._dc_chat) 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. """Return encryption info for this chat.
:returns: a string with encryption preferences of all chat members""" :returns: a string with encryption preferences of all chat members"""
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id) res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
return from_dc_charpointer(res) 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. """ get/create Join-Group QR Code as ascii-string.
this string needs to be transferred to another DC account this string needs to be transferred to another DC account
@@ -194,7 +195,7 @@ class Chat(object):
# ------ chat messaging API ------------------------------ # ------ chat messaging API ------------------------------
def send_msg(self, msg): def send_msg(self, msg: Message) -> Message:
"""send a message by using a ready Message object. """send a message by using a ready Message object.
:param msg: a :class:`deltachat.message.Message` instance :param msg: a :class:`deltachat.message.Message` instance

View File

@@ -1,7 +1,13 @@
from typing import Any, List
from .capi import lib from .capi import lib
for name in dir(lib): def __getattr__(name: str) -> Any:
if name.startswith("DC_"): if name.startswith("DC_"):
globals()[name] = getattr(lib, name) return getattr(lib, name)
del name return globals()[name]
def __dir__() -> List[str]:
return sorted(name for name in dir(lib) if name.startswith("DC_"))

View File

@@ -1,6 +1,9 @@
from .capi import lib from .capi import lib
from .capi import ffi from .capi import ffi
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional, TypeVar, Generator, Callable
T = TypeVar('T')
def as_dc_charpointer(obj): def as_dc_charpointer(obj):
@@ -11,21 +14,22 @@ def as_dc_charpointer(obj):
return 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)): for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
yield constructor(lib.dc_array_get_id(dc_array_t, i)) 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: if obj != ffi.NULL:
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8") return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
return None
class DCLot: class DCLot:
def __init__(self, dc_lot): def __init__(self, dc_lot) -> None:
self._dc_lot = dc_lot self._dc_lot = dc_lot
def id(self): def id(self) -> int:
return lib.dc_lot_get_id(self._dc_lot) return lib.dc_lot_get_id(self._dc_lot)
def state(self): def state(self):

View File

@@ -11,7 +11,7 @@ from imapclient import IMAPClient
from imapclient.exceptions import IMAPClientError from imapclient.exceptions import IMAPClientError
import imaplib import imaplib
import deltachat import deltachat
from deltachat import const from deltachat import const, Account
SEEN = b'\\Seen' SEEN = b'\\Seen'
@@ -62,7 +62,7 @@ def dc_account_after_shutdown(account):
class DirectImap: class DirectImap:
def __init__(self, account): def __init__(self, account: Account) -> None:
self.account = account self.account = account
self.logid = account.get_config("displayname") or id(account) self.logid = account.get_config("displayname") or id(account)
self._idling = False self._idling = False

View File

@@ -13,7 +13,7 @@ from .cutil import from_dc_charpointer
class FFIEvent: class FFIEvent:
def __init__(self, name, data1, data2): def __init__(self, name: str, data1, data2):
self.name = name self.name = name
self.data1 = data1 self.data1 = data1
self.data2 = data2 self.data2 = data2
@@ -29,13 +29,13 @@ class FFIEventLogger:
# to prevent garbled logging # to prevent garbled logging
_loglock = threading.RLock() _loglock = threading.RLock()
def __init__(self, account): def __init__(self, account) -> None:
self.account = account self.account = account
self.logid = self.account.get_config("displayname") self.logid = self.account.get_config("displayname")
self.init_time = time.time() self.init_time = time.time()
@account_hookimpl @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)) self.account.log(str(ffi_event))
@account_hookimpl @account_hookimpl
@@ -69,7 +69,7 @@ class FFIEventTracker:
self._event_queue = Queue() self._event_queue = Queue()
@account_hookimpl @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) self._event_queue.put(ffi_event)
def set_timeout(self, timeout): def set_timeout(self, timeout):
@@ -96,7 +96,7 @@ class FFIEventTracker:
if rex.match(ev.name): if rex.match(ev.name):
return ev return ev
def get_info_contains(self, regex): def get_info_contains(self, regex: str) -> FFIEvent:
rex = re.compile(regex) rex = re.compile(regex)
while 1: while 1:
ev = self.get_matching("DC_EVENT_INFO") ev = self.get_matching("DC_EVENT_INFO")
@@ -176,6 +176,7 @@ class FFIEventTracker:
ev = self.get_matching("DC_EVENT_MSGS_CHANGED") ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0: if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2) return self.account.get_message_by_id(ev.data2)
return None
def wait_msg_delivered(self, msg): def wait_msg_delivered(self, msg):
ev = self.get_matching("DC_EVENT_MSG_DELIVERED") 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. With each Account init this callback thread is started.
""" """
def __init__(self, account): def __init__(self, account) -> None:
self.account = account self.account = account
super(EventThread, self).__init__(name="events") super(EventThread, self).__init__(name="events")
self.setDaemon(True) self.setDaemon(True)
@@ -202,17 +203,17 @@ class EventThread(threading.Thread):
yield yield
self.account.log(message + " FINISHED") self.account.log(message + " FINISHED")
def mark_shutdown(self): def mark_shutdown(self) -> None:
self._marked_for_shutdown = True self._marked_for_shutdown = True
def wait(self, timeout=None): def wait(self, timeout=None) -> None:
if self == threading.current_thread(): if self == threading.current_thread():
# we are in the callback thread and thus cannot # we are in the callback thread and thus cannot
# wait for the thread-loop to finish. # wait for the thread-loop to finish.
return return
self.join(timeout=timeout) self.join(timeout=timeout)
def run(self): def run(self) -> None:
""" get and run events until shutdown. """ """ get and run events until shutdown. """
with self.log_execution("EVENT THREAD"): with self.log_execution("EVENT THREAD"):
self._inner_run() self._inner_run()
@@ -250,7 +251,7 @@ class EventThread(threading.Thread):
if self.account._dc_context is not None: if self.account._dc_context is not None:
raise raise
def _map_ffi_event(self, ffi_event): def _map_ffi_event(self, ffi_event: FFIEvent):
name = ffi_event.name name = ffi_event.name
account = self.account account = self.account
if name == "DC_EVENT_CONFIGURE_PROGRESS": if name == "DC_EVENT_CONFIGURE_PROGRESS":

View File

@@ -14,7 +14,7 @@ class Provider(object):
:param domain: The email to get the provider info for. :param domain: The email to get the provider info for.
""" """
def __init__(self, account, addr): def __init__(self, account, addr) -> None:
provider = ffi.gc( provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)), lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_unref, lib.dc_provider_unref,

View File

@@ -9,6 +9,7 @@ import fnmatch
import time import time
import weakref import weakref
import tempfile import tempfile
from typing import List, Dict, Callable
import pytest import pytest
import requests import requests
@@ -126,7 +127,7 @@ def pytest_report_header(config, startdir):
class SessionLiveConfigFromFile: class SessionLiveConfigFromFile:
def __init__(self, fn): def __init__(self, fn) -> None:
self.fn = fn self.fn = fn
self.configlist = [] self.configlist = []
for line in open(fn): for line in open(fn):
@@ -137,19 +138,21 @@ class SessionLiveConfigFromFile:
d[name] = value d[name] = value
self.configlist.append(d) self.configlist.append(d)
def get(self, index): def get(self, index: int):
return self.configlist[index] return self.configlist[index]
def exists(self): def exists(self) -> bool:
return bool(self.configlist) return bool(self.configlist)
class SessionLiveConfigFromURL: class SessionLiveConfigFromURL:
def __init__(self, url): configlist: List[Dict[str, str]]
def __init__(self, url: str) -> None:
self.configlist = [] self.configlist = []
self.url = url self.url = url
def get(self, index): def get(self, index: int):
try: try:
return self.configlist[index] return self.configlist[index]
except IndexError: except IndexError:
@@ -162,7 +165,7 @@ class SessionLiveConfigFromURL:
self.configlist.append(config) self.configlist.append(config)
return config return config
def exists(self): def exists(self) -> bool:
return bool(self.configlist) return bool(self.configlist)
@@ -179,7 +182,7 @@ def session_liveconfig(request):
@pytest.fixture @pytest.fixture
def data(request): def data(request):
class Data: class Data:
def __init__(self): def __init__(self) -> None:
# trying to find test data heuristically # trying to find test data heuristically
# because we are run from a dev-setup with pytest direct, # because we are run from a dev-setup with pytest direct,
# through tox, and then maybe also from deltachat-binding # through tox, and then maybe also from deltachat-binding
@@ -210,7 +213,10 @@ def data(request):
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class AccountMaker: class AccountMaker:
def __init__(self): _finalizers: List[Callable[[], None]]
_accounts: List[Account]
def __init__(self) -> None:
self.live_count = 0 self.live_count = 0
self.offline_count = 0 self.offline_count = 0
self._finalizers = [] self._finalizers = []
@@ -423,7 +429,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
pass pass
imap.dump_imap_structures(tmpdir, logfile=logfile) 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) ac2.create_chat(ac1)
return ac1.create_chat(ac2) return ac1.create_chat(ac2)
@@ -451,7 +457,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class BotProcess: class BotProcess:
def __init__(self, popen, bot_cfg): stdout_queue: queue.Queue
def __init__(self, popen, bot_cfg) -> None:
self.popen = popen self.popen = popen
self.addr = bot_cfg["addr"] self.addr = bot_cfg["addr"]
@@ -459,10 +467,10 @@ class BotProcess:
# the (unicode) lines available for readers through a queue. # the (unicode) lines available for readers through a queue.
self.stdout_queue = queue.Queue() self.stdout_queue = queue.Queue()
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread") self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
t.setDaemon(1) t.setDaemon(True)
t.start() t.start()
def _run_stdout_thread(self): def _run_stdout_thread(self) -> None:
try: try:
while 1: while 1:
line = self.popen.stdout.readline() line = self.popen.stdout.readline()
@@ -474,10 +482,10 @@ class BotProcess:
finally: finally:
self.stdout_queue.put(None) self.stdout_queue.put(None)
def kill(self): def kill(self) -> None:
self.popen.kill() self.popen.kill()
def wait(self, timeout=30): def wait(self, timeout=30) -> None:
self.popen.wait(timeout=timeout) self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines): def fnmatch_lines(self, pattern_lines):
@@ -509,14 +517,14 @@ def tmp_db_path(tmpdir):
@pytest.fixture @pytest.fixture
def lp(): def lp():
class Printer: class Printer:
def sec(self, msg): def sec(self, msg: str) -> None:
print() print()
print("=" * 10, msg, "=" * 10) print("=" * 10, msg, "=" * 10)
def step(self, msg): def step(self, msg: str) -> None:
print("-" * 5, "step " + msg, "-" * 5) print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg): def indent(self, msg: str) -> None:
print(" " + msg) print(" " + msg)
return Printer() return Printer()

View File

@@ -3,6 +3,7 @@ isolated_build = true
envlist = envlist =
py3 py3
lint lint
mypy
auditwheels auditwheels
[testenv] [testenv]
@@ -43,6 +44,15 @@ commands =
flake8 tests/ examples/ flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst rst-lint --encoding 'utf-8' README.rst
[testenv:mypy]
deps =
mypy
typing
types-setuptools
types-requests
commands =
mypy --no-incremental src/
[testenv:doc] [testenv:doc]
changedir=doc changedir=doc
deps = deps =