diff --git a/python/examples/echo_and_quit.py b/python/examples/echo_and_quit.py index e6ae69b7f..6f32f96ba 100644 --- a/python/examples/echo_and_quit.py +++ b/python/examples/echo_and_quit.py @@ -1,4 +1,3 @@ - # content of echo_and_quit.py from deltachat import account_hookimpl, run_cmdline @@ -15,7 +14,9 @@ class EchoPlugin: message.create_chat() addr = message.get_sender_contact().addr if message.is_system_message(): - message.chat.send_text("echoing system message from {}:\n{}".format(addr, message)) + message.chat.send_text( + "echoing system message from {}:\n{}".format(addr, message) + ) else: text = message.text message.chat.send_text("echoing from {}:\n{}".format(addr, text)) diff --git a/python/examples/group_tracking.py b/python/examples/group_tracking.py index b8aee428b..c5ebaed42 100644 --- a/python/examples/group_tracking.py +++ b/python/examples/group_tracking.py @@ -1,4 +1,3 @@ - # content of group_tracking.py from deltachat import account_hookimpl, run_cmdline @@ -33,15 +32,21 @@ class GroupTrackingPlugin: @account_hookimpl def ac_member_added(self, chat, contact, actor, message): - print("ac_member_added {} to chat {} from {}".format( - contact.addr, chat.id, actor or message.get_sender_contact().addr)) + print( + "ac_member_added {} to chat {} from {}".format( + contact.addr, chat.id, actor or message.get_sender_contact().addr + ) + ) for member in chat.get_contacts(): print("chat member: {}".format(member.addr)) @account_hookimpl def ac_member_removed(self, chat, contact, actor, message): - print("ac_member_removed {} from chat {} by {}".format( - contact.addr, chat.id, actor or message.get_sender_contact().addr)) + print( + "ac_member_removed {} from chat {} by {}".format( + contact.addr, chat.id, actor or message.get_sender_contact().addr + ) + ) def main(argv=None): diff --git a/python/examples/test_examples.py b/python/examples/test_examples.py index 196b970cb..3978417bf 100644 --- a/python/examples/test_examples.py +++ b/python/examples/test_examples.py @@ -1,20 +1,20 @@ - -import pytest -import py import echo_and_quit import group_tracking +import py +import pytest + from deltachat.events import FFIEventLogger -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def datadir(): """The py.path.local object of the test-data/ directory.""" for path in reversed(py.path.local(__file__).parts()): - datadir = path.join('test-data') + datadir = path.join("test-data") if datadir.isdir(): return datadir else: - pytest.skip('test-data directory not found') + pytest.skip("test-data directory not found") def test_echo_quit_plugin(acfactory, lp): @@ -22,7 +22,7 @@ def test_echo_quit_plugin(acfactory, lp): botproc = acfactory.run_bot_process(echo_and_quit) lp.sec("creating a temp account to contact the bot") - ac1, = acfactory.get_online_accounts(1) + (ac1,) = acfactory.get_online_accounts(1) lp.sec("sending a message to the bot") bot_contact = ac1.create_contact(botproc.addr) @@ -44,9 +44,11 @@ def test_group_tracking_plugin(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) - botproc.fnmatch_lines(""" + botproc.fnmatch_lines( + """ *ac_configure_completed* - """) + """ + ) ac1.add_account_plugin(FFIEventLogger(ac1)) ac2.add_account_plugin(FFIEventLogger(ac2)) @@ -56,9 +58,11 @@ def test_group_tracking_plugin(acfactory, lp): ch.add_contact(bot_contact) ch.send_text("hello") - botproc.fnmatch_lines(""" + botproc.fnmatch_lines( + """ *ac_chat_modified*bot test group* - """) + """ + ) lp.sec("adding third member {}".format(ac2.get_config("addr"))) contact3 = ac1.create_contact(ac2.get_config("addr")) @@ -68,12 +72,20 @@ def test_group_tracking_plugin(acfactory, lp): assert "hello" in reply.text lp.sec("now looking at what the bot received") - botproc.fnmatch_lines(""" + botproc.fnmatch_lines( + """ *ac_member_added {}*from*{}* - """.format(contact3.addr, ac1.get_config("addr"))) + """.format( + contact3.addr, ac1.get_config("addr") + ) + ) lp.sec("contact successfully added, now removing") ch.remove_contact(contact3) - botproc.fnmatch_lines(""" + botproc.fnmatch_lines( + """ *ac_member_removed {}*from*{}* - """.format(contact3.addr, ac1.get_config("addr"))) + """.format( + contact3.addr, ac1.get_config("addr") + ) + ) diff --git a/python/src/deltachat/__init__.py b/python/src/deltachat/__init__.py index 0633f8c1f..ebff11e0b 100644 --- a/python/src/deltachat/__init__.py +++ b/python/src/deltachat/__init__.py @@ -1,15 +1,15 @@ import sys -from . import capi, const, hookspec # noqa -from .capi import ffi # noqa -from .account import Account, get_core_info # noqa -from .message import Message # noqa -from .contact import Contact # noqa -from .chat import Chat # noqa -from .hookspec import account_hookimpl, global_hookimpl # noqa -from . import events +from pkg_resources import DistributionNotFound, get_distribution + +from . import capi, const, events, hookspec # noqa +from .account import Account, get_core_info # noqa +from .capi import ffi # noqa +from .chat import Chat # noqa +from .contact import Contact # noqa +from .hookspec import account_hookimpl, global_hookimpl # noqa +from .message import Message # noqa -from pkg_resources import get_distribution, DistributionNotFound try: __version__ = get_distribution(__name__).version except DistributionNotFound: @@ -26,7 +26,7 @@ def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}): def register_global_plugin(plugin): - """ Register a global plugin which implements one or more + """Register a global plugin which implements one or more of the :class:`deltachat.hookspec.Global` hooks. """ gm = hookspec.Global._get_plugin_manager() @@ -43,15 +43,18 @@ register_global_plugin(events) def run_cmdline(argv=None, account_plugins=None): - """ Run a simple default command line app, registering the specified - account plugins. """ + """Run a simple default command line app, registering the specified + account plugins.""" import argparse + if argv is None: argv = sys.argv parser = argparse.ArgumentParser(prog=argv[0] if argv else None) parser.add_argument("db", action="store", help="database file") - parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events") + parser.add_argument( + "--show-ffi", action="store_true", help="show low level ffi events" + ) parser.add_argument("--email", action="store", help="email address") parser.add_argument("--password", action="store", help="password") @@ -69,9 +72,9 @@ def run_cmdline(argv=None, account_plugins=None): ac.add_account_plugin(plugin) if not ac.is_configured(): - assert args.email and args.password, ( - "you must specify --email and --password once to configure this database/account" - ) + assert ( + args.email and args.password + ), "you must specify --email and --password once to configure this database/account" ac.set_config("addr", args.email) ac.set_config("mail_pw", args.password) ac.set_config("mvbox_move", "0") diff --git a/python/src/deltachat/_build.py b/python/src/deltachat/_build.py index 2e64d57d4..9c9876cff 100644 --- a/python/src/deltachat/_build.py +++ b/python/src/deltachat/_build.py @@ -20,30 +20,35 @@ def local_build_flags(projdir, target): :param target: The rust build target, `debug` or `release`. """ flags = {} - if platform.system() == 'Darwin': - flags['libraries'] = ['resolv', 'dl'] - flags['extra_link_args'] = [ - '-framework', 'CoreFoundation', - '-framework', 'CoreServices', - '-framework', 'Security', + if platform.system() == "Darwin": + flags["libraries"] = ["resolv", "dl"] + flags["extra_link_args"] = [ + "-framework", + "CoreFoundation", + "-framework", + "CoreServices", + "-framework", + "Security", ] - elif platform.system() == 'Linux': - flags['libraries'] = ['rt', 'dl', 'm'] - flags['extra_link_args'] = [] + elif platform.system() == "Linux": + flags["libraries"] = ["rt", "dl", "m"] + flags["extra_link_args"] = [] else: - raise NotImplementedError("Compilation not supported yet on Windows, can you help?") + raise NotImplementedError( + "Compilation not supported yet on Windows, can you help?" + ) target_dir = os.environ.get("CARGO_TARGET_DIR") if target_dir is None: - target_dir = os.path.join(projdir, 'target') - flags['extra_objects'] = [os.path.join(target_dir, target, 'libdeltachat.a')] - assert os.path.exists(flags['extra_objects'][0]), flags['extra_objects'] - flags['include_dirs'] = [os.path.join(projdir, 'deltachat-ffi')] + target_dir = os.path.join(projdir, "target") + flags["extra_objects"] = [os.path.join(target_dir, target, "libdeltachat.a")] + assert os.path.exists(flags["extra_objects"][0]), flags["extra_objects"] + flags["include_dirs"] = [os.path.join(projdir, "deltachat-ffi")] return flags def system_build_flags(): """Construct build flags for building against an installed libdeltachat.""" - return pkgconfig.parse('deltachat') + return pkgconfig.parse("deltachat") def extract_functions(flags): @@ -61,11 +66,13 @@ def extract_functions(flags): src_name = os.path.join(tmpdir, "include.h") dst_name = os.path.join(tmpdir, "expanded.h") with open(src_name, "w") as src_fp: - src_fp.write('#include ') - cc.preprocess(source=src_name, - output_file=dst_name, - include_dirs=flags['include_dirs'], - macros=[('PY_CFFI', '1')]) + src_fp.write("#include ") + cc.preprocess( + source=src_name, + output_file=dst_name, + include_dirs=flags["include_dirs"], + macros=[("PY_CFFI", "1")], + ) with open(dst_name, "r") as dst_fp: return dst_fp.read() finally: @@ -87,7 +94,9 @@ def find_header(flags): obj_name = os.path.join(tmpdir, "where.o") dst_name = os.path.join(tmpdir, "where") with open(src_name, "w") as src_fp: - src_fp.write(textwrap.dedent(""" + src_fp.write( + textwrap.dedent( + """ #include #include @@ -95,18 +104,22 @@ def find_header(flags): printf("%s", _dc_header_file_location()); return 0; } - """)) + """ + ) + ) cwd = os.getcwd() try: os.chdir(tmpdir) - cc.compile(sources=["where.c"], - include_dirs=flags['include_dirs'], - macros=[("PY_CFFI_INC", "1")]) + cc.compile( + sources=["where.c"], + include_dirs=flags["include_dirs"], + macros=[("PY_CFFI_INC", "1")], + ) finally: os.chdir(cwd) - cc.link_executable(objects=[obj_name], - output_progname="where", - output_dir=tmpdir) + cc.link_executable( + objects=[obj_name], output_progname="where", output_dir=tmpdir + ) return subprocess.check_output(dst_name) finally: shutil.rmtree(tmpdir) @@ -123,7 +136,8 @@ def extract_defines(flags): cdef() method. """ header = find_header(flags) - defines_re = re.compile(r""" + defines_re = re.compile( + r""" \#define\s+ # The start of a define. ( # Begin capturing group which captures the define name. (?: # A nested group which is not captured, this allows us @@ -151,26 +165,28 @@ def extract_defines(flags): ) # Close the capturing group, this contains # the entire name e.g. DC_MSG_TEXT. \s+\S+ # Ensure there is whitespace followed by a value. - """, re.VERBOSE) + """, + re.VERBOSE, + ) defines = [] with open(header) as fp: for line in fp: match = defines_re.match(line) if match: defines.append(match.group(1)) - return '\n'.join('#define {} ...'.format(d) for d in defines) + return "\n".join("#define {} ...".format(d) for d in defines) def ffibuilder(): - projdir = os.environ.get('DCC_RS_DEV') + projdir = os.environ.get("DCC_RS_DEV") if projdir: - target = os.environ.get('DCC_RS_TARGET', 'release') + target = os.environ.get("DCC_RS_TARGET", "release") flags = local_build_flags(projdir, target) else: flags = system_build_flags() builder = cffi.FFI() builder.set_source( - 'deltachat.capi', + "deltachat.capi", """ #include int dc_event_has_string_data(int e) @@ -180,11 +196,13 @@ def ffibuilder(): """, **flags, ) - builder.cdef(""" + builder.cdef( + """ typedef int... time_t; void free(void *ptr); extern int dc_event_has_string_data(int); - """) + """ + ) function_defs = extract_functions(flags) defines = extract_defines(flags) builder.cdef(function_defs) @@ -192,8 +210,9 @@ def ffibuilder(): return builder -if __name__ == '__main__': +if __name__ == "__main__": import os.path - pkgdir = os.path.join(os.path.dirname(__file__), '..') + + pkgdir = os.path.join(os.path.dirname(__file__), "..") builder = ffibuilder() builder.compile(tmpdir=pkgdir, verbose=True) diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index 77c7cb8fc..f07dd8496 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -1,37 +1,48 @@ """ Account class implementation. """ from __future__ import print_function + +import os +from array import array from contextlib import contextmanager from email.utils import parseaddr from threading import Event -import os -from array import array -from . import const +from typing import Any, Dict, Generator, List, Optional, Union + +from . import const, hookspec from .capi import ffi, lib -from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot from .chat import Chat -from .message import Message from .contact import Contact -from .tracker import ImexTracker, ConfigureTracker -from . import hookspec +from .cutil import ( + DCLot, + as_dc_charpointer, + from_dc_charpointer, + from_optional_dc_charpointer, + iter_array, +) from .events import EventThread -from typing import Union, Any, Dict, Optional, List, Generator +from .message import Message +from .tracker import ConfigureTracker, ImexTracker class MissingCredentials(ValueError): - """ Account is missing `addr` and `mail_pw` config values. """ + """Account is missing `addr` and `mail_pw` config values.""" def get_core_info(): - """ get some system info. """ + """get some system info.""" from tempfile import NamedTemporaryFile with NamedTemporaryFile() as path: path.close() - return get_dc_info_as_dict(ffi.gc( - lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL), - lib.dc_context_unref, - )) + return get_dc_info_as_dict( + ffi.gc( + lib.dc_context_new( + as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL + ), + lib.dc_context_unref, + ) + ) def get_dc_info_as_dict(dc_context): @@ -46,14 +57,15 @@ def get_dc_info_as_dict(dc_context): class Account(object): - """ Each account is tied to a sqlite database file which is fully managed + """Each account is tied to a sqlite database file which is fully managed by the underlying deltachat core library. All public Account methods are meant to be memory-safe and return memory-safe objects. """ + MissingCredentials = MissingCredentials 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 will be created if it doesn't exist. @@ -83,11 +95,11 @@ class Account(object): hook.dc_account_init(account=self) def disable_logging(self) -> None: - """ disable logging. """ + """disable logging.""" self._logging = False def enable_logging(self) -> None: - """ re-enable logging. """ + """re-enable logging.""" self._logging = True def __repr__(self): @@ -102,11 +114,14 @@ class Account(object): 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)) + raise KeyError( + "{!r} not a valid config key, existing keys: {!r}".format( + name, self._configkeys + ) + ) def get_info(self) -> Dict[str, str]: - """ return dictionary of built config parameters. """ + """return dictionary of built config parameters.""" return get_dc_info_as_dict(self._dc_context) def dump_account_info(self, logfile): @@ -126,7 +141,7 @@ class Account(object): log("") 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 value: string to set as new transalation @@ -138,7 +153,7 @@ class Account(object): raise ValueError("could not set translation string") def set_config(self, name: str, value: Optional[str]) -> None: - """ set configuration values. + """set configuration values. :param name: config key name (unicode) :param value: value to set (unicode) @@ -157,7 +172,7 @@ class Account(object): lib.dc_set_config(self._dc_context, namebytes, valuebytes) def get_config(self, name: str) -> str: - """ return unicode string value. + """return unicode string value. :param name: configuration key to lookup (eg "addr" or "mail_pw") :returns: unicode value @@ -175,15 +190,17 @@ class Account(object): In other words, you don't need this. """ - res = lib.dc_preconfigure_keypair(self._dc_context, - as_dc_charpointer(addr), - as_dc_charpointer(public), - as_dc_charpointer(secret)) + res = lib.dc_preconfigure_keypair( + self._dc_context, + as_dc_charpointer(addr), + as_dc_charpointer(public), + as_dc_charpointer(secret), + ) if res == 0: raise Exception("Failed to set key") def update_config(self, kwargs: Dict[str, Any]) -> None: - """ update config values. + """update config values. :param kwargs: name=value config settings for this account. values need to be unicode. @@ -193,7 +210,7 @@ class Account(object): self.set_config(key, value) 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. :returns: True if account is configured. @@ -219,18 +236,17 @@ class Account(object): self.set_config("selfavatar", img_path) 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(): raise ValueError("need to configure first") 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)) return from_optional_dc_charpointer(res) def get_blobdir(self) -> str: - """ return the directory for files. + """return the directory for files. All sent files are copied to this directory if necessary. Place files there directly to avoid copying. @@ -238,7 +254,7 @@ class Account(object): return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context)) 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` """ @@ -280,14 +296,14 @@ class Account(object): elif isinstance(obj, str): displayname, addr = parseaddr(obj) else: - raise TypeError("don't know how to create chat for %r" % (obj, )) + raise TypeError("don't know how to create chat for %r" % (obj,)) if name is None and displayname: name = displayname return (name, addr) def delete_contact(self, contact: Contact) -> bool: - """ delete a Contact. + """delete a Contact. :param contact: contact object obtained :returns: True if deletion succeeded (contact was deleted) @@ -298,7 +314,7 @@ class Account(object): return bool(lib.dc_delete_contact(self._dc_context, contact_id)) 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 = as_dc_charpointer(addr) contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr) @@ -307,20 +323,19 @@ class Account(object): return None def get_contact_by_id(self, contact_id: int) -> Contact: - """ return Contact instance or raise an exception. + """return Contact instance or raise an exception. :param contact_id: integer id of this contact. :returns: :class:`deltachat.contact.Contact` instance. """ return Contact(self, contact_id) 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. """ dc_array = ffi.gc( - lib.dc_get_blocked_contacts(self._dc_context), - lib.dc_array_unref + lib.dc_get_blocked_contacts(self._dc_context), lib.dc_array_unref ) return list(iter_array(dc_array, lambda x: Contact(self, x))) @@ -345,21 +360,17 @@ class Account(object): if with_self: flags |= const.DC_GCL_ADD_SELF dc_array = ffi.gc( - lib.dc_get_contacts(self._dc_context, flags, query), - lib.dc_array_unref + lib.dc_get_contacts(self._dc_context, flags, query), lib.dc_array_unref ) return list(iter_array(dc_array, lambda x: Contact(self, x))) 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), - lib.dc_array_unref - ) + """yield all fresh messages from all chats.""" + dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref) yield from iter_array(dc_array, lambda x: Message.from_db(self, x)) 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() def create_group_chat( @@ -385,13 +396,12 @@ class Account(object): return chat def get_chats(self) -> List[Chat]: - """ return list of chats. + """return list of chats. :returns: a list of :class:`deltachat.chat.Chat` objects. """ dc_chatlist = ffi.gc( - lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0), - lib.dc_chatlist_unref + lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0), lib.dc_chatlist_unref ) assert dc_chatlist != ffi.NULL @@ -405,14 +415,14 @@ class Account(object): return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat() def get_message_by_id(self, msg_id: int) -> Message: - """ return Message instance. + """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: int) -> Chat: - """ return Chat instance. + """return Chat instance. :param chat_id: integer id of this chat. :returns: :class:`deltachat.chat.Chat` instance. :raises: ValueError if chat does not exist. @@ -424,7 +434,7 @@ class Account(object): return Chat(self, chat_id) 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. """ @@ -438,7 +448,7 @@ class Account(object): lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages)) 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 chat: :class:`deltachat.chat.Chat` object. @@ -448,7 +458,7 @@ class Account(object): lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id) 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. :returns: None @@ -457,7 +467,7 @@ class Account(object): lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids)) def export_self_keys(self, path): - """ export public and private keys to the specified directory. + """export public and private keys to the specified directory. Note that the account does not have to be started. """ @@ -481,7 +491,7 @@ class Account(object): return imex_tracker.wait_finish() def import_self_keys(self, path): - """ Import private keys found in the `path` directory. + """Import private keys found in the `path` directory. The last imported key is made the default keys unless its name contains the string legacy. Public keys are not imported. @@ -517,7 +527,7 @@ class Account(object): return from_dc_charpointer(res) def get_setup_contact_qr(self) -> 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 in a second channel (typically used by mobiles with QRcode-show + scan UX) @@ -527,10 +537,9 @@ class Account(object): return from_dc_charpointer(res) def check_qr(self, qr): - """ check qr code and return :class:`ScannedQRCode` instance representing the result""" + """check qr code and return :class:`ScannedQRCode` instance representing the result""" res = ffi.gc( - lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), - lib.dc_lot_unref + lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref ) lot = DCLot(res) if lot.state() == const.DC_QR_ERROR: @@ -538,7 +547,7 @@ class Account(object): return ScannedQRCode(lot) def qr_setup_contact(self, qr): - """ setup contact and return a Chat after contact is established. + """setup contact and return a Chat after contact is established. Note that this function may block for a long time as messages are exchanged with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance @@ -552,7 +561,7 @@ class Account(object): return Chat(self, chat_id) def qr_join_chat(self, qr): - """ join a chat group through a QR code. + """join a chat group through a QR code. Note that this function may block for a long time as messages are exchanged with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance @@ -587,7 +596,7 @@ class Account(object): # def add_account_plugin(self, plugin, name=None): - """ add an account plugin which implements one or more of + """add an account plugin which implements one or more of the :class:`deltachat.hookspec.PerAccount` hooks. """ if name and self._pm.has_plugin(name=name): @@ -597,18 +606,18 @@ class Account(object): return plugin def remove_account_plugin(self, plugin, name=None): - """ remove an account plugin. """ + """remove an account plugin.""" self._pm.unregister(plugin, name=name) @contextmanager def temp_plugin(self, plugin): - """ run a with-block with the given plugin temporarily registered. """ + """run a with-block with the given plugin temporarily registered.""" self._pm.register(plugin) yield plugin self._pm.unregister(plugin) def stop_ongoing(self): - """ Stop ongoing securejoin, configuration or other core jobs. """ + """Stop ongoing securejoin, configuration or other core jobs.""" lib.dc_stop_ongoing_process(self._dc_context) def get_connectivity(self): @@ -621,7 +630,7 @@ class Account(object): return lib.dc_all_work_done(self._dc_context) def start_io(self): - """ start this account's IO scheduling (Rust-core async scheduler) + """start this account's IO scheduling (Rust-core async scheduler) If this account is not configured an Exception is raised. You need to call account.configure() and account.wait_configure_finish() @@ -665,7 +674,7 @@ class Account(object): lib.dc_maybe_network(self._dc_context) 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 value for the configuration process. """ @@ -678,11 +687,11 @@ class Account(object): return configtracker 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() 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.stop_ongoing() @@ -690,7 +699,7 @@ class Account(object): lib.dc_stop_io(self._dc_context) 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).""" if self._dc_context is None: return diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index 501d22e1a..d39860ce3 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -1,32 +1,41 @@ """ Chat and Location related API. """ -import mimetypes import calendar import json -from datetime import datetime, timezone +import mimetypes import os -from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array -from .capi import lib, ffi -from . import const -from .message import Message +from datetime import datetime, timezone from typing import Optional +from . import const +from .capi import ffi, lib +from .cutil import ( + as_dc_charpointer, + from_dc_charpointer, + from_optional_dc_charpointer, + iter_array, +) +from .message import Message + class Chat(object): - """ Chat object which manages members and through which you can send and retrieve messages. + """Chat object which manages members and through which you can send and retrieve messages. You obtain instances of it through :class:`deltachat.account.Account`. """ 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) -> bool: - return self.id == getattr(other, "id", None) and \ - self.account._dc_context == other.account._dc_context + return ( + self.id == getattr(other, "id", None) + and self.account._dc_context == other.account._dc_context + ) def __ne__(self, other) -> bool: return not (self == other) @@ -37,8 +46,7 @@ class Chat(object): @property def _dc_chat(self): return ffi.gc( - lib.dc_get_chat(self.account._dc_context, self.id), - lib.dc_chat_unref + lib.dc_get_chat(self.account._dc_context, self.id), lib.dc_chat_unref ) def delete(self) -> None: @@ -62,28 +70,28 @@ class Chat(object): # ------ chat status/metadata API ------------------------------ 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 otherwise """ return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP 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. """ return lib.dc_chat_is_muted(self._dc_chat) def is_contact_request(self): - """ return True if this chat is a contact request chat. + """return True if this chat is a contact request chat. :returns: True if chat is a contact request chat, False otherwise. """ return lib.dc_chat_is_contact_request(self._dc_chat) def is_promoted(self): - """ return True if this chat is promoted, i.e. + """return True if this chat is promoted, i.e. the member contacts are aware of their membership, have been sent messages. @@ -100,21 +108,21 @@ class Chat(object): return lib.dc_chat_can_send(self._dc_chat) 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. """ return lib.dc_chat_is_protected(self._dc_chat) def get_name(self) -> Optional[str]: - """ return name of this chat. + """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: str) -> bool: - """ set name of this chat. + """set name of this chat. :param name: as a unicode string. :returns: True on success, False otherwise @@ -123,7 +131,7 @@ class Chat(object): return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name)) 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. :returns: None @@ -132,12 +140,14 @@ class Chat(object): mute_duration = -1 else: mute_duration = duration - ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration) + ret = lib.dc_set_chat_mute_duration( + self.account._dc_context, self.id, mute_duration + ) if not bool(ret): raise ValueError("Call to dc_set_chat_mute_duration failed") def unmute(self) -> None: - """ unmutes the chat + """unmutes the chat :returns: None """ @@ -146,7 +156,7 @@ class Chat(object): raise ValueError("Failed to unmute chat") 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: :returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted) @@ -154,23 +164,25 @@ class Chat(object): return lib.dc_chat_get_remaining_mute_duration(self._dc_chat) def get_ephemeral_timer(self) -> int: - """ get ephemeral timer. + """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: int) -> bool: - """ set ephemeral timer. + """set ephemeral timer. :param: timer value in seconds :returns: True on success, False otherwise """ - return bool(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) -> int: - """ (deprecated) return type of this chat. + """(deprecated) return type of this chat. :returns: one of const.DC_CHAT_TYPE_* """ @@ -184,7 +196,7 @@ class Chat(object): return from_dc_charpointer(res) 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 in a second channel (typically used by mobiles with QRcode-show + scan UX) @@ -220,7 +232,7 @@ class Chat(object): return msg def send_text(self, text): - """ send a text message and return the resulting Message instance. + """send a text message and return the resulting Message instance. :param msg: unicode text :raises ValueError: if message can not be send/chat does not exist. @@ -233,7 +245,7 @@ class Chat(object): return Message.from_db(self.account, msg_id) def send_file(self, path, mime_type="application/octet-stream"): - """ send a file and return the resulting Message instance. + """send a file and return the resulting Message instance. :param path: path to the file. :param mime_type: the mime-type of this file, defaults to application/octet-stream. @@ -248,7 +260,7 @@ class Chat(object): return Message.from_db(self.account, sent_id) def send_image(self, path): - """ send an image message and return the resulting Message instance. + """send an image message and return the resulting Message instance. :param path: path to an image file. :raises ValueError: if message can not be send/chat does not exist. @@ -263,7 +275,7 @@ class Chat(object): return Message.from_db(self.account, sent_id) def prepare_message(self, msg): - """ prepare a message for sending. + """prepare a message for sending. :param msg: the message to be prepared. :returns: a :class:`deltachat.message.Message` instance. @@ -278,7 +290,7 @@ class Chat(object): return msg def prepare_message_file(self, path, mime_type=None, view_type="file"): - """ prepare a message for sending and return the resulting Message instance. + """prepare a message for sending and return the resulting Message instance. To actually send the message, call :meth:`send_prepared`. The file must be inside the blob directory. @@ -294,7 +306,7 @@ class Chat(object): return self.prepare_message(msg) def send_prepared(self, message): - """ send a previously prepared message. + """send a previously prepared message. :param message: a :class:`Message` instance previously returned by :meth:`prepare_file`. @@ -314,7 +326,7 @@ class Chat(object): msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg def set_draft(self, message): - """ set message as draft. + """set message as draft. :param message: a :class:`Message` instance :returns: None @@ -325,7 +337,7 @@ class Chat(object): lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg) def get_draft(self): - """ get draft message for this chat. + """get draft message for this chat. :param message: a :class:`Message` instance :returns: Message object or None (if no draft available) @@ -337,32 +349,32 @@ class Chat(object): return Message(self.account, dc_msg) def get_messages(self): - """ return list of messages in this chat. + """return list of messages in this chat. :returns: list of :class:`deltachat.message.Message` objects for this chat. """ dc_array = ffi.gc( lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0), - lib.dc_array_unref + lib.dc_array_unref, ) return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x))) def count_fresh_messages(self): - """ return number of fresh messages in this chat. + """return number of fresh messages in this chat. :returns: number of fresh messages """ return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id) def mark_noticed(self): - """ mark all messages in this chat as noticed. + """mark all messages in this chat as noticed. Noticed messages are no longer fresh. """ return lib.dc_marknoticed_chat(self.account._dc_context, self.id) def get_summary(self): - """ return dictionary with summary information. """ + """return dictionary with summary information.""" dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id) s = from_dc_charpointer(dc_res) return json.loads(s) @@ -370,7 +382,7 @@ class Chat(object): # ------ group management API ------------------------------ def add_contact(self, obj): - """ add a contact to this chat. + """add a contact to this chat. :params obj: Contact, Account or e-mail address. :raises ValueError: if contact could not be added @@ -383,35 +395,36 @@ class Chat(object): return contact def remove_contact(self, obj): - """ remove a contact from this chat. + """remove a contact from this chat. :params obj: Contact, Account or e-mail address. :raises ValueError: if contact could not be removed :returns: None """ contact = self.account.get_contact(obj) - ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id) + ret = lib.dc_remove_contact_from_chat( + self.account._dc_context, self.id, contact.id + ) if ret != 1: raise ValueError("could not remove contact {!r} from chat".format(contact)) def get_contacts(self): - """ get all contacts for this chat. + """get all contacts for this chat. :returns: list of :class:`deltachat.contact.Contact` objects for this chat """ from .contact import Contact + dc_array = ffi.gc( lib.dc_get_chat_contacts(self.account._dc_context, self.id), - lib.dc_array_unref - ) - return list(iter_array( - dc_array, lambda id: Contact(self.account, id)) + lib.dc_array_unref, ) + return list(iter_array(dc_array, lambda id: Contact(self.account, id))) def num_contacts(self): - """ return number of contacts in this chat. """ + """return number of contacts in this chat.""" dc_array = ffi.gc( lib.dc_get_chat_contacts(self.account._dc_context, self.id), - lib.dc_array_unref + lib.dc_array_unref, ) return lib.dc_array_get_cnt(dc_array) @@ -476,7 +489,10 @@ class Chat(object): """return True if this chat is archived. :returns: True if archived. """ - return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED + return ( + lib.dc_chat_get_visibility(self._dc_chat) + == const.DC_CHAT_VISIBILITY_ARCHIVED + ) def enable_sending_locations(self, seconds): """enable sending locations for this chat. @@ -507,17 +523,20 @@ class Chat(object): else: contact_id = contact.id - dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to) + dc_array = lib.dc_get_locations( + self.account._dc_context, self.id, contact_id, time_from, time_to + ) return [ Location( latitude=lib.dc_array_get_latitude(dc_array, i), longitude=lib.dc_array_get_longitude(dc_array, i), accuracy=lib.dc_array_get_accuracy(dc_array, i), timestamp=datetime.fromtimestamp( - lib.dc_array_get_timestamp(dc_array, i), - timezone.utc + lib.dc_array_get_timestamp(dc_array, i), timezone.utc + ), + marker=from_optional_dc_charpointer( + lib.dc_array_get_marker(dc_array, i) ), - marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)), ) for i in range(lib.dc_array_get_cnt(dc_array)) ] diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index 83e415081..142be93d8 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -10,40 +10,46 @@ from .cutil import from_dc_charpointer, from_optional_dc_charpointer class Contact(object): - """ Delta-Chat Contact. + """Delta-Chat Contact. You obtain instances of it through :class:`deltachat.account.Account`. """ + def __init__(self, account, id): from .account import Account + assert isinstance(account, Account), repr(account) self.account = account self.id = id def __eq__(self, other): - return self.account._dc_context == other.account._dc_context and self.id == other.id + return ( + self.account._dc_context == other.account._dc_context + and self.id == other.id + ) def __ne__(self, other): return not (self == other) def __repr__(self): - return "".format(self.id, self.addr, self.account._dc_context) + return "".format( + self.id, self.addr, self.account._dc_context + ) @property def _dc_contact(self): return ffi.gc( - lib.dc_get_contact(self.account._dc_context, self.id), - lib.dc_contact_unref + lib.dc_get_contact(self.account._dc_context, self.id), lib.dc_contact_unref ) @props.with_doc def addr(self) -> str: - """ normalized e-mail address for this account. """ + """normalized e-mail address for this account.""" return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact)) @props.with_doc def name(self) -> str: - """ display name for this contact. """ + """display name for this contact.""" return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact)) # deprecated alias @@ -57,23 +63,23 @@ class Contact(object): ) def is_blocked(self): - """ Return True if the contact is blocked. """ + """Return True if the contact is blocked.""" return lib.dc_contact_is_blocked(self._dc_contact) def set_blocked(self, block=True): - """ [Deprecated, use block/unblock methods] Block or unblock a contact. """ + """[Deprecated, use block/unblock methods] Block or unblock a contact.""" return lib.dc_block_contact(self.account._dc_context, self.id, block) def block(self): - """ Block this contact. Message will not be seen/retrieved from this contact. """ + """Block this contact. Message will not be seen/retrieved from this contact.""" return lib.dc_block_contact(self.account._dc_context, self.id, True) def unblock(self): - """ Unblock this contact. Messages from this contact will be retrieved (again).""" + """Unblock this contact. Messages from this contact will be retrieved (again).""" return lib.dc_block_contact(self.account._dc_context, self.id, False) def is_verified(self): - """ Return True if the contact is verified. """ + """Return True if the contact is verified.""" return lib.dc_contact_is_verified(self._dc_contact) def get_profile_image(self) -> Optional[str]: @@ -93,7 +99,7 @@ class Contact(object): return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact)) def create_chat(self): - """ create or get an existing 1:1 chat object for the specified contact or contact id. + """create or get an existing 1:1 chat object for the specified contact or contact id. :param contact: chat_id (int) or contact object. :returns: a :class:`deltachat.chat.Chat` object. diff --git a/python/src/deltachat/cutil.py b/python/src/deltachat/cutil.py index 1a91e6a8e..ad9810219 100644 --- a/python/src/deltachat/cutil.py +++ b/python/src/deltachat/cutil.py @@ -1,9 +1,9 @@ -from .capi import lib -from .capi import ffi from datetime import datetime, timezone -from typing import Optional, TypeVar, Generator, Callable +from typing import Callable, Generator, Optional, TypeVar -T = TypeVar('T') +from .capi import ffi, lib + +T = TypeVar("T") def as_dc_charpointer(obj): diff --git a/python/src/deltachat/direct_imap.py b/python/src/deltachat/direct_imap.py index 94943cb71..c407af103 100644 --- a/python/src/deltachat/direct_imap.py +++ b/python/src/deltachat/direct_imap.py @@ -3,18 +3,27 @@ Internal Python-level IMAP handling used by the testplugin and for cleaning up inbox/mvbox for each test function run. """ -import io -import ssl -import pathlib -from contextlib import contextmanager -from imap_tools import MailBox, MailBoxTls, errors, AND, Header, MailMessageFlags, MailMessage import imaplib -from deltachat import const, Account +import io +import pathlib +import ssl +from contextlib import contextmanager from typing import List +from imap_tools import ( + AND, + Header, + MailBox, + MailBoxTls, + MailMessage, + MailMessageFlags, + errors, +) -FLAGS = b'FLAGS' -FETCH = b'FETCH' +from deltachat import Account, const + +FLAGS = b"FLAGS" +FETCH = b"FETCH" ALL = "1:*" @@ -69,8 +78,8 @@ class DirectImap: return self.conn.folder.set(foldername) def select_config_folder(self, config_name: str): - """ Return info about selected folder if it is - configured, otherwise None. """ + """Return info about selected folder if it is + configured, otherwise None.""" if "_" not in config_name: config_name = "configured_{}_folder".format(config_name) foldername = self.account.get_config(config_name) @@ -78,17 +87,17 @@ class DirectImap: return self.select_folder(foldername) def list_folders(self) -> List[str]: - """ return list of all existing folder names""" + """return list of all existing folder names""" assert not self._idling return [folder.name for folder in self.conn.folder.list()] def delete(self, uid_list: str, expunge=True): - """ delete a range of messages (imap-syntax). + """delete a range of messages (imap-syntax). If expunge is true, perform the expunge-operation to make sure the messages are really gone and not just flagged as deleted. """ - self.conn.client.uid('STORE', uid_list, '+FLAGS', r'(\Deleted)') + self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)") if expunge: self.conn.expunge() @@ -141,7 +150,13 @@ class DirectImap: fn = path.joinpath(str(msg.uid)) fn.write_bytes(body) log("Message", msg.uid, fn) - log("Message", msg.uid, msg.flags, "Message-Id:", msg.obj.get("Message-Id")) + log( + "Message", + msg.uid, + msg.flags, + "Message-Id:", + msg.obj.get("Message-Id"), + ) if empty_folders: log("--------- EMPTY FOLDERS:", empty_folders) @@ -150,7 +165,7 @@ class DirectImap: @contextmanager def idle(self): - """ return Idle ContextManager. """ + """return Idle ContextManager.""" idle_manager = IdleManager(self) try: yield idle_manager @@ -163,13 +178,20 @@ class DirectImap: """ if msg.startswith("\n"): msg = msg[1:] - msg = '\n'.join([s.lstrip() for s in msg.splitlines()]) - self.conn.append(bytes(msg, encoding='ascii'), folder) + msg = "\n".join([s.lstrip() for s in msg.splitlines()]) + self.conn.append(bytes(msg, encoding="ascii"), folder) def get_uid_by_message_id(self, message_id) -> str: - msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header('MESSAGE-ID', message_id)))] + msgs = [ + msg.uid + for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id))) + ] if len(msgs) == 0: - raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?") + raise Exception( + "Did not find message " + + message_id + + ", maybe you forgot to select the correct folder?" + ) return msgs[0] @@ -183,7 +205,7 @@ class IdleManager: self.direct_imap.conn.idle.start() def check(self, timeout=None) -> List[bytes]: - """ (blocking) wait for next idle message from server. """ + """(blocking) wait for next idle message from server.""" self.log("imap-direct: calling idle_check") res = self.direct_imap.conn.idle.poll(timeout=timeout) self.log("imap-direct: idle_check returned {!r}".format(res)) @@ -192,20 +214,19 @@ class IdleManager: def wait_for_new_message(self, timeout=None) -> bytes: while 1: for item in self.check(timeout=timeout): - if b'EXISTS' in item or b'RECENT' in item: + if b"EXISTS" in item or b"RECENT" in item: return item def wait_for_seen(self, timeout=None) -> int: - """ Return first message with SEEN flag from a running idle-stream. - """ + """Return first message with SEEN flag from a running idle-stream.""" while 1: for item in self.check(timeout=timeout): if FETCH in item: self.log(str(item)) - if FLAGS in item and rb'\Seen' in item: - return int(item.split(b' ')[1]) + if FLAGS in item and rb"\Seen" in item: + return int(item.split(b" ")[1]) def done(self): - """ send idle-done to server if we are currently in idle mode. """ + """send idle-done to server if we are currently in idle mode.""" res = self.direct_imap.conn.idle.stop() return res diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py index ad1f12189..8bc0b4961 100644 --- a/python/src/deltachat/events.py +++ b/python/src/deltachat/events.py @@ -1,18 +1,19 @@ -import threading -import sys -import traceback -import time import io -import re import os -from queue import Queue, Empty +import re +import sys +import threading +import time +import traceback +from contextlib import contextmanager +from queue import Empty, Queue import deltachat -from .hookspec import account_hookimpl -from contextlib import contextmanager + from .capi import ffi, lib -from .message import map_system_message from .cutil import from_optional_dc_charpointer +from .hookspec import account_hookimpl +from .message import map_system_message class FFIEvent: @@ -26,9 +27,10 @@ class FFIEvent: class FFIEventLogger: - """ If you register an instance of this logger with an Account + """If you register an instance of this logger with an Account you'll get all ffi-events printed. """ + # to prevent garbled logging _loglock = threading.RLock() @@ -56,9 +58,9 @@ class FFIEventLogger: s = "{:2.2f} [{}] {}".format(elapsed, locname, message) if os.name == "posix": - WARN = '\033[93m' - ERROR = '\033[91m' - ENDC = '\033[0m' + WARN = "\033[93m" + ERROR = "\033[91m" + ENDC = "\033[0m" if message.startswith("DC_EVENT_WARNING"): s = WARN + s + ENDC if message.startswith("DC_EVENT_ERROR"): @@ -133,7 +135,12 @@ class FFIEventTracker: if current == expected_next: return elif current != previous: - raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current)) + raise Exception( + "Expected connectivity " + + str(expected_next) + + " but got " + + str(current) + ) self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED") @@ -171,12 +178,12 @@ class FFIEventTracker: self.get_info_contains("INBOX: Idle entering") def wait_next_incoming_message(self): - """ wait for and return next incoming message. """ + """wait for and return next incoming message.""" ev = self.get_matching("DC_EVENT_INCOMING_MSG") return self.account.get_message_by_id(ev.data2) def wait_next_messages_changed(self): - """ wait for and return next message-changed message or None + """wait for and return next message-changed message or None if the event contains no msgid""" ev = self.get_matching("DC_EVENT_MSGS_CHANGED") if ev.data2 > 0: @@ -191,10 +198,11 @@ class FFIEventTracker: class EventThread(threading.Thread): - """ Event Thread for an account. + """Event Thread for an account. With each Account init this callback thread is started. """ + def __init__(self, account) -> None: self.account = account super(EventThread, self).__init__(name="events") @@ -219,7 +227,7 @@ class EventThread(threading.Thread): self.join(timeout=timeout) def run(self) -> None: - """ get and run events until shutdown. """ + """get and run events until shutdown.""" with self.log_execution("EVENT THREAD"): self._inner_run() @@ -244,8 +252,12 @@ class EventThread(threading.Thread): lib.dc_event_unref(event) ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2) - with self.swallow_and_log_exception("ac_process_ffi_event {}".format(ffi_event)): - self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event) + with self.swallow_and_log_exception( + "ac_process_ffi_event {}".format(ffi_event) + ): + self.account._pm.hook.ac_process_ffi_event( + account=self, ffi_event=ffi_event + ) for name, kwargs in self._map_ffi_event(ffi_event): hook = getattr(self.account._pm.hook, name) info = "call {} kwargs={} failed".format(name, kwargs) @@ -259,8 +271,9 @@ class EventThread(threading.Thread): except Exception as ex: logfile = io.StringIO() traceback.print_exception(*sys.exc_info(), file=logfile) - self.account.log("{}\nException {}\nTraceback:\n{}" - .format(info, ex, logfile.getvalue())) + self.account.log( + "{}\nException {}\nTraceback:\n{}".format(info, ex, logfile.getvalue()) + ) def _map_ffi_event(self, ffi_event: FFIEvent): name = ffi_event.name @@ -282,7 +295,10 @@ class EventThread(threading.Thread): yield res yield "ac_outgoing_message", dict(message=msg) elif msg.is_in_fresh(): - yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg)) + yield map_system_message(msg) or ( + "ac_incoming_message", + dict(message=msg), + ) elif name == "DC_EVENT_MSG_DELIVERED": msg = account.get_message_by_id(ffi_event.data2) yield "ac_message_delivered", dict(message=msg) diff --git a/python/src/deltachat/hookspec.py b/python/src/deltachat/hookspec.py index 27ce6b085..d8992a572 100644 --- a/python/src/deltachat/hookspec.py +++ b/python/src/deltachat/hookspec.py @@ -2,7 +2,6 @@ import pluggy - account_spec_name = "deltachat-account" account_hookspec = pluggy.HookspecMarker(account_spec_name) account_hookimpl = pluggy.HookimplMarker(account_spec_name) @@ -13,12 +12,13 @@ global_hookimpl = pluggy.HookimplMarker(global_spec_name) class PerAccount: - """ per-Account-instance hook specifications. + """per-Account-instance hook specifications. All hooks are executed in a dedicated Event thread. Hooks are generally not allowed to block/last long as this blocks overall event processing on the python side. """ + @classmethod def _make_plugin_manager(cls): pm = pluggy.PluginManager(account_spec_name) @@ -27,7 +27,7 @@ class PerAccount: @account_hookspec def ac_process_ffi_event(self, ffi_event): - """ process a CFFI low level events for a given account. + """process a CFFI low level events for a given account. ffi_event has "name", "data1", "data2" values as specified with `DC_EVENT_* `_. @@ -35,37 +35,37 @@ class PerAccount: @account_hookspec def ac_log_line(self, message): - """ log a message related to the account. """ + """log a message related to the account.""" @account_hookspec def ac_configure_completed(self, success): - """ Called after a configure process completed. """ + """Called after a configure process completed.""" @account_hookspec def ac_incoming_message(self, message): - """ Called on any incoming message (both existing chats and contact requests). """ + """Called on any incoming message (both existing chats and contact requests).""" @account_hookspec def ac_outgoing_message(self, message): - """ Called on each outgoing message (both system and "normal").""" + """Called on each outgoing message (both system and "normal").""" @account_hookspec def ac_message_delivered(self, message): - """ Called when an outgoing message has been delivered to SMTP. + """Called when an outgoing message has been delivered to SMTP. :param message: Message that was just delivered. """ @account_hookspec def ac_chat_modified(self, chat): - """ Chat was created or modified regarding membership, avatar, title. + """Chat was created or modified regarding membership, avatar, title. :param chat: Chat which was modified. """ @account_hookspec def ac_member_added(self, chat, contact, actor, message): - """ Called for each contact added to an accepted chat. + """Called for each contact added to an accepted chat. :param chat: Chat where contact was added. :param contact: Contact that was added. @@ -75,7 +75,7 @@ class PerAccount: @account_hookspec def ac_member_removed(self, chat, contact, actor, message): - """ Called for each contact removed from a chat. + """Called for each contact removed from a chat. :param chat: Chat where contact was removed. :param contact: Contact that was removed. @@ -85,10 +85,11 @@ class PerAccount: class Global: - """ global hook specifications using a per-process singleton + """global hook specifications using a per-process singleton plugin manager instance. """ + _plugin_manager = None @classmethod @@ -100,11 +101,11 @@ class Global: @global_hookspec def dc_account_init(self, account): - """ called when `Account::__init__()` function starts executing. """ + """called when `Account::__init__()` function starts executing.""" @global_hookspec def dc_account_extra_configure(self, account): - """ Called when account configuration successfully finished. + """Called when account configuration successfully finished. This hook can be used to perform extra work before ac_configure_completed is called. @@ -112,4 +113,4 @@ class Global: @global_hookspec def dc_account_after_shutdown(self, account): - """ Called after the account has been shutdown. """ + """Called after the account has been shutdown.""" diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index d5aa939b4..40036a11f 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -2,20 +2,21 @@ import os import re -from . import props -from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer -from .capi import lib, ffi -from . import const from datetime import datetime, timezone from typing import Optional +from . import const, props +from .capi import ffi, lib +from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer + class Message(object): - """ Message object. + """Message object. You obtain instances of it through :class:`deltachat.account.Account` or :class:`deltachat.chat.Chat`. """ + def __init__(self, account, dc_msg): self.account = account assert isinstance(self.account._dc_context, ffi.CData) @@ -32,20 +33,26 @@ class Message(object): c = self.get_sender_contact() typ = "outgoing" if self.is_outgoing() else "incoming" return "".format( - typ, self.is_system_message(), repr(self.text[:10]), - self.id, c.id, c.addr, self.chat.id, self.chat.get_name()) + typ, + self.is_system_message(), + repr(self.text[:10]), + self.id, + c.id, + c.addr, + self.chat.id, + self.chat.get_name(), + ) @classmethod def from_db(cls, account, id): assert id > 0 - return cls(account, ffi.gc( - lib.dc_get_msg(account._dc_context, id), - lib.dc_msg_unref - )) + return cls( + account, ffi.gc(lib.dc_get_msg(account._dc_context, id), lib.dc_msg_unref) + ) @classmethod def new_empty(cls, account, view_type): - """ create a non-persistent message. + """create a non-persistent message. :param: view_type is the message type code or one of the strings: "text", "audio", "video", "file", "sticker" @@ -54,13 +61,15 @@ class Message(object): view_type_code = view_type else: view_type_code = get_viewtype_code_from_name(view_type) - return Message(account, ffi.gc( - lib.dc_msg_new(account._dc_context, view_type_code), - lib.dc_msg_unref - )) + return Message( + account, + ffi.gc( + lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref + ), + ) def create_chat(self): - """ create or get an existing chat (group) object for this message. + """create or get an existing chat (group) object for this message. If the message is a contact request the sender will become an accepted contact. @@ -72,23 +81,27 @@ class Message(object): @props.with_doc def id(self): - """id of this message. """ + """id of this message.""" return lib.dc_msg_get_id(self._dc_msg) @props.with_doc def text(self) -> str: - """unicode text of this messages (might be empty if not a text message). """ + """unicode text of this messages (might be empty if not a text message).""" return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg)) def set_text(self, text): - """set text of this message. """ + """set text of this message.""" lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text)) @props.with_doc def html(self) -> str: - """html text of this messages (might be empty if not an html message). """ - return from_optional_dc_charpointer( - lib.dc_get_msg_html(self.account._dc_context, self.id)) or "" + """html text of this messages (might be empty if not an html message).""" + return ( + from_optional_dc_charpointer( + lib.dc_get_msg_html(self.account._dc_context, self.id) + ) + or "" + ) def has_html(self): """return True if this message has an html part, False otherwise.""" @@ -103,11 +116,11 @@ class Message(object): @props.with_doc def filename(self): - """filename if there was an attachment, otherwise empty string. """ + """filename if there was an attachment, otherwise empty string.""" return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg)) def set_file(self, path, mime_type=None): - """set file for this message from path and mime_type. """ + """set file for this message from path and mime_type.""" mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type) if not os.path.exists(path): raise ValueError("path does not exist: {!r}".format(path)) @@ -115,7 +128,7 @@ class Message(object): @props.with_doc def basename(self) -> str: - """basename of the attachment if it exists, otherwise empty string. """ + """basename of the attachment if it exists, otherwise empty string.""" # FIXME, it does not return basename return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg)) @@ -125,42 +138,42 @@ class Message(object): return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg)) def is_system_message(self): - """ return True if this message is a system/info message. """ + """return True if this message is a system/info message.""" return bool(lib.dc_msg_is_info(self._dc_msg)) def is_setup_message(self): - """ return True if this message is a setup message. """ + """return True if this message is a setup message.""" return lib.dc_msg_is_setupmessage(self._dc_msg) def get_setupcodebegin(self) -> str: - """ return the first characters of a setup code in a setup message. """ + """return the first characters of a setup code in a setup message.""" return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg)) def is_encrypted(self): - """ return True if this message was encrypted. """ + """return True if this message was encrypted.""" return bool(lib.dc_msg_get_showpadlock(self._dc_msg)) def is_bot(self): - """ return True if this message is submitted automatically. """ + """return True if this message is submitted automatically.""" return bool(lib.dc_msg_is_bot(self._dc_msg)) def is_forwarded(self): - """ return True if this message was forwarded. """ + """return True if this message was forwarded.""" return bool(lib.dc_msg_is_forwarded(self._dc_msg)) def get_message_info(self) -> str: - """ Return informational text for a single message. + """Return informational text for a single message. The text is multiline and may contain eg. the raw text of the message. """ - return from_dc_charpointer(lib.dc_get_msg_info(self.account._dc_context, self.id)) + return from_dc_charpointer( + lib.dc_get_msg_info(self.account._dc_context, self.id) + ) def continue_key_transfer(self, setup_code): - """ extract key and use it as primary key for this account. """ + """extract key and use it as primary key for this account.""" res = lib.dc_continue_key_transfer( - self.account._dc_context, - self.id, - as_dc_charpointer(setup_code) + self.account._dc_context, self.id, as_dc_charpointer(setup_code) ) if res == 0: raise ValueError("could not decrypt") @@ -230,7 +243,7 @@ class Message(object): lib.dc_msg_force_plaintext(self._dc_msg) def get_mime_headers(self): - """ return mime-header object for an incoming message. + """return mime-header object for an incoming message. This only returns a non-None object if ``save_mime_headers`` config option was set and the message is incoming. @@ -238,6 +251,7 @@ class Message(object): :returns: email-mime message object (with headers only, no body). """ import email.parser + mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id) if mime_headers: s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref)) @@ -257,6 +271,7 @@ class Message(object): :returns: :class:`deltachat.chat.Chat` object """ from .chat import Chat + chat_id = lib.dc_msg_get_chat_id(self._dc_msg) return Chat(self.account, chat_id) @@ -267,12 +282,12 @@ class Message(object): Usually used to impersonate someone else. """ return from_optional_dc_charpointer( - lib.dc_msg_get_override_sender_name(self._dc_msg)) + lib.dc_msg_get_override_sender_name(self._dc_msg) + ) def set_override_sender_name(self, name): - """set different sender name for a message. """ - lib.dc_msg_set_override_sender_name( - self._dc_msg, as_dc_charpointer(name)) + """set different sender name for a message.""" + lib.dc_msg_set_override_sender_name(self._dc_msg, as_dc_charpointer(name)) def get_sender_chat(self): """return the 1:1 chat with the sender of this message. @@ -287,6 +302,7 @@ class Message(object): :returns: :class:`deltachat.chat.Contact` instance """ from .contact import Contact + contact_id = lib.dc_msg_get_from_id(self._dc_msg) return Contact(self.account, contact_id) @@ -300,13 +316,12 @@ class Message(object): else: # load message from db to get a fresh/current state dc_msg = ffi.gc( - lib.dc_get_msg(self.account._dc_context, self.id), - lib.dc_msg_unref + lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref ) return lib.dc_msg_get_state(dc_msg) def is_in_fresh(self): - """ return True if Message is incoming fresh message (un-noticed). + """return True if Message is incoming fresh message (un-noticed). Fresh messages are not noticed nor seen and are typically shown in notifications. @@ -330,25 +345,25 @@ class Message(object): return self._msgstate == const.DC_STATE_IN_SEEN def is_outgoing(self): - """Return True if Message is outgoing. """ + """Return True if Message is outgoing.""" return self._msgstate in ( - const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING, - const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD, - const.DC_STATE_OUT_DELIVERED) + const.DC_STATE_OUT_PREPARING, + const.DC_STATE_OUT_PENDING, + const.DC_STATE_OUT_FAILED, + const.DC_STATE_OUT_MDN_RCVD, + const.DC_STATE_OUT_DELIVERED, + ) def is_out_preparing(self): - """Return True if Message is outgoing, but its file is being prepared. - """ + """Return True if Message is outgoing, but its file is being prepared.""" return self._msgstate == const.DC_STATE_OUT_PREPARING def is_out_pending(self): - """Return True if Message is outgoing, but is pending (no single checkmark). - """ + """Return True if Message is outgoing, but is pending (no single checkmark).""" return self._msgstate == const.DC_STATE_OUT_PENDING def is_out_failed(self): - """Return True if Message is unrecoverably failed. - """ + """Return True if Message is unrecoverably failed.""" return self._msgstate == const.DC_STATE_OUT_FAILED def is_out_delivered(self): @@ -375,48 +390,48 @@ class Message(object): return lib.dc_msg_get_viewtype(self._dc_msg) def is_text(self): - """ return True if it's a text message. """ + """return True if it's a text message.""" return self._view_type == const.DC_MSG_TEXT def is_image(self): - """ return True if it's an image message. """ + """return True if it's an image message.""" return self._view_type == const.DC_MSG_IMAGE def is_gif(self): - """ return True if it's a gif message. """ + """return True if it's a gif message.""" return self._view_type == const.DC_MSG_GIF def is_sticker(self): - """ return True if it's a sticker message. """ + """return True if it's a sticker message.""" return self._view_type == const.DC_MSG_STICKER def is_audio(self): - """ return True if it's an audio message. """ + """return True if it's an audio message.""" return self._view_type == const.DC_MSG_AUDIO def is_video(self): - """ return True if it's a video message. """ + """return True if it's a video message.""" return self._view_type == const.DC_MSG_VIDEO def is_file(self): - """ return True if it's a file message. """ + """return True if it's a file message.""" return self._view_type == const.DC_MSG_FILE def mark_seen(self): - """ mark this message as seen. """ + """mark this message as seen.""" self.account.mark_seen_messages([self.id]) # some code for handling DC_MSG_* view types _view_type_mapping = { - 'text': const.DC_MSG_TEXT, - 'image': const.DC_MSG_IMAGE, - 'gif': const.DC_MSG_GIF, - 'audio': const.DC_MSG_AUDIO, - 'video': const.DC_MSG_VIDEO, - 'file': const.DC_MSG_FILE, - 'sticker': const.DC_MSG_STICKER, + "text": const.DC_MSG_TEXT, + "image": const.DC_MSG_IMAGE, + "gif": const.DC_MSG_GIF, + "audio": const.DC_MSG_AUDIO, + "video": const.DC_MSG_VIDEO, + "file": const.DC_MSG_FILE, + "sticker": const.DC_MSG_STICKER, } @@ -424,14 +439,17 @@ def get_viewtype_code_from_name(view_type_name): code = _view_type_mapping.get(view_type_name) if code is not None: return code - raise ValueError("message typecode not found for {!r}, " - "available {!r}".format(view_type_name, list(_view_type_mapping.keys()))) + raise ValueError( + "message typecode not found for {!r}, " + "available {!r}".format(view_type_name, list(_view_type_mapping.keys())) + ) # # some helper code for turning system messages into hook events # + def map_system_message(msg): if msg.is_system_message(): res = parse_system_add_remove(msg.text) @@ -448,7 +466,7 @@ def map_system_message(msg): def extract_addr(text): - m = re.match(r'.*\((.+@.+)\)', text) + m = re.match(r".*\((.+@.+)\)", text) if m: text = m.group(1) text = text.rstrip(".") @@ -456,9 +474,9 @@ def extract_addr(text): def parse_system_add_remove(text): - """ return add/remove info from parsing the given system message text. + """return add/remove info from parsing the given system message text. - returns a (action, affected, actor) triple """ + returns a (action, affected, actor) triple""" # Member Me (x@y) removed by a@b. # Member x@y added by a@b @@ -467,7 +485,7 @@ def parse_system_add_remove(text): # Group left by some one (tmp1@x.org). # Group left by tmp1@x.org. text = text.lower() - m = re.match(r'member (.+) (removed|added) by (.+)', text) + m = re.match(r"member (.+) (removed|added) by (.+)", text) if m: affected, action, actor = m.groups() return action, extract_addr(affected), extract_addr(actor) diff --git a/python/src/deltachat/props.py b/python/src/deltachat/props.py index 3190e6dd3..ab21794ae 100644 --- a/python/src/deltachat/props.py +++ b/python/src/deltachat/props.py @@ -9,6 +9,7 @@ def with_doc(f): # https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py def cached(f): """returns a cached property that is calculated by function f""" + def get(self): try: return self._property_cache[f] diff --git a/python/src/deltachat/provider.py b/python/src/deltachat/provider.py index ed90784b4..2797fce84 100644 --- a/python/src/deltachat/provider.py +++ b/python/src/deltachat/provider.py @@ -16,7 +16,9 @@ class Provider(object): 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_new_from_email( + account._dc_context, as_dc_charpointer(addr) + ), lib.dc_provider_unref, ) if provider == ffi.NULL: @@ -26,14 +28,14 @@ class Provider(object): @property def overview_page(self) -> str: """URL to the overview page of the provider on providers.delta.chat.""" - return from_dc_charpointer( - lib.dc_provider_get_overview_page(self._provider)) + return from_dc_charpointer(lib.dc_provider_get_overview_page(self._provider)) @property def get_before_login_hints(self) -> str: """Should be shown to the user on login.""" return from_dc_charpointer( - lib.dc_provider_get_before_login_hint(self._provider)) + lib.dc_provider_get_before_login_hint(self._provider) + ) @property def status(self) -> int: diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index ca532ff9b..1a0cb7b6f 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -1,56 +1,63 @@ from __future__ import print_function -import os -import sys -import io -import subprocess -import queue -import threading + import fnmatch +import io +import os +import pathlib +import queue +import subprocess +import sys +import threading import time import weakref from queue import Queue -from typing import List, Callable +from typing import Callable, List import pytest import requests -import pathlib - -from . import Account, const, account_hookimpl, get_core_info -from .events import FFIEventLogger, FFIEventTracker from _pytest._code import Source import deltachat +from . import Account, account_hookimpl, const, get_core_info +from .events import FFIEventLogger, FFIEventTracker + def pytest_addoption(parser): group = parser.getgroup("deltachat testplugin options") group.addoption( - "--liveconfig", action="store", default=None, + "--liveconfig", + action="store", + default=None, help="a file with >=2 lines where each line " - "contains NAME=VALUE config settings for one account" + "contains NAME=VALUE config settings for one account", ) group.addoption( - "--ignored", action="store_true", + "--ignored", + action="store_true", help="Also run tests marked with the ignored marker", ) group.addoption( - "--strict-tls", action="store_true", + "--strict-tls", + action="store_true", help="Never accept invalid TLS certificates for test accounts", ) group.addoption( - "--extra-info", action="store_true", - help="show more info on failures (imap server state, config)" + "--extra-info", + action="store_true", + help="show more info on failures (imap server state, config)", ) group.addoption( - "--debug-setup", action="store_true", - help="show events during configure and start io phases of online accounts" + "--debug-setup", + action="store_true", + help="show events during configure and start io phases of online accounts", ) def pytest_configure(config): - cfg = config.getoption('--liveconfig') + cfg = config.getoption("--liveconfig") if not cfg: - cfg = os.getenv('DCC_NEW_TMP_EMAIL') + cfg = os.getenv("DCC_NEW_TMP_EMAIL") if cfg: config.option.liveconfig = cfg @@ -113,19 +120,21 @@ def pytest_configure(config): def pytest_report_header(config, startdir): info = get_core_info() - summary = ['Deltachat core={} sqlite={} journal_mode={}'.format( - info['deltachat_core_version'], - info['sqlite_version'], - info['journal_mode'], - )] + summary = [ + "Deltachat core={} sqlite={} journal_mode={}".format( + info["deltachat_core_version"], + info["sqlite_version"], + info["journal_mode"], + ) + ] cfg = config.option.liveconfig if cfg: if "?" in cfg: url, token = cfg.split("?", 1) - summary.append('Liveconfig provider: {}?'.format(url)) + summary.append("Liveconfig provider: {}?".format(url)) else: - summary.append('Liveconfig file: {}'.format(cfg)) + summary.append("Liveconfig file: {}".format(cfg)) return summary @@ -135,26 +144,28 @@ def testprocess(request): class TestProcess: - """ A pytest session-scoped instance to help with managing "live" account configurations. - """ + """A pytest session-scoped instance to help with managing "live" account configurations.""" + def __init__(self, pytestconfig): self.pytestconfig = pytestconfig self._addr2files = {} self._configlist = [] def get_liveconfig_producer(self): - """ provide live account configs, cached on a per-test-process scope + """provide live account configs, cached on a per-test-process scope so that test functions can re-use already known live configs. Depending on the --liveconfig option this comes from a HTTP provider or a file with a line specifying each accounts config. """ liveconfig_opt = self.pytestconfig.getoption("--liveconfig") if not liveconfig_opt: - pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts") + pytest.skip( + "specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts" + ) if not liveconfig_opt.startswith("http"): for line in open(liveconfig_opt): - if line.strip() and not line.strip().startswith('#'): + if line.strip() and not line.strip().startswith("#"): d = {} for part in line.split(): name, value = part.split("=") @@ -170,14 +181,21 @@ class TestProcess: except IndexError: res = requests.post(liveconfig_opt) if res.status_code != 200: - pytest.fail("newtmpuser count={} code={}: '{}'".format( - index, res.status_code, res.text)) + pytest.fail( + "newtmpuser count={} code={}: '{}'".format( + index, res.status_code, res.text + ) + ) d = res.json() config = dict(addr=d["email"], mail_pw=d["password"]) print("newtmpuser {}: addr={}".format(index, config["addr"])) self._configlist.append(config) yield config - pytest.fail("more than {} live accounts requested.".format(MAX_LIVE_CREATED_ACCOUNTS)) + pytest.fail( + "more than {} live accounts requested.".format( + MAX_LIVE_CREATED_ACCOUNTS + ) + ) def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path): db_target_path = pathlib.Path(db_target_path) @@ -230,13 +248,18 @@ def data(request): # because we are run from a dev-setup with pytest direct, # through tox, and then maybe also from deltachat-binding # users like "deltabot". - self.paths = [os.path.normpath(x) for x in [ - os.path.join(os.path.dirname(request.fspath.strpath), "data"), - os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data") - ]] + self.paths = [ + os.path.normpath(x) + for x in [ + os.path.join(os.path.dirname(request.fspath.strpath), "data"), + os.path.join( + os.path.dirname(__file__), "..", "..", "..", "test-data" + ), + ] + ] def get_path(self, bn): - """ return path of file or None if it doesn't exist. """ + """return path of file or None if it doesn't exist.""" for path in self.paths: fn = os.path.join(path, *bn.split("/")) if os.path.exists(fn): @@ -253,10 +276,11 @@ def data(request): class ACSetup: - """ accounts setup helper to deal with multiple configure-process + """accounts setup helper to deal with multiple configure-process and io & imap initialization phases. From tests, use the higher level public ACFactory methods instead of its private helper class. """ + CONFIGURING = "CONFIGURING" CONFIGURED = "CONFIGURED" IDLEREADY = "IDLEREADY" @@ -272,13 +296,16 @@ class ACSetup: print("[acsetup]", "{:.3f}".format(time.time() - self.init_time), *args) def add_configured(self, account): - """ add an already configured account. """ + """add an already configured account.""" assert account.is_configured() self._account2state[account] = self.CONFIGURED - self.log("added already configured account", account, account.get_config("addr")) + self.log( + "added already configured account", account, account.get_config("addr") + ) def start_configure(self, account, reconfigure=False): - """ add an account and start its configure process. """ + """add an account and start its configure process.""" + class PendingTracker: @account_hookimpl def ac_configure_completed(this, success): @@ -290,7 +317,7 @@ class ACSetup: self.log("started configure on", account) def wait_one_configured(self, account): - """ wait until this account has successfully configured. """ + """wait until this account has successfully configured.""" if self._account2state[account] == self.CONFIGURING: while 1: acc = self._pop_config_success() @@ -301,7 +328,7 @@ class ACSetup: acc._evtracker.consume_events() def bring_online(self): - """ Wait for all accounts to become ready to receive messages. + """Wait for all accounts to become ready to receive messages. This will initialize logging, start IO and the direct_imap attribute for each account which either is CONFIGURED already or which is CONFIGURING @@ -336,12 +363,12 @@ class ACSetup: acc.log("inbox IDLE ready") def init_logging(self, acc): - """ idempotent function for initializing logging (will replace existing logger). """ + """idempotent function for initializing logging (will replace existing logger).""" logger = FFIEventLogger(acc, logid=acc._logid, init_time=self.init_time) acc.add_account_plugin(logger, name="logger-" + acc._logid) def init_imap(self, acc): - """ initialize direct_imap and cleanup server state. """ + """initialize direct_imap and cleanup server state.""" from deltachat.direct_imap import DirectImap assert acc.is_configured() @@ -375,8 +402,7 @@ class ACFactory: self._finalizers = [] self._accounts = [] self._acsetup = ACSetup(testprocess, self.init_time) - self._preconfigured_keys = ["alice", "bob", "charlie", - "dom", "elena", "fiona"] + self._preconfigured_keys = ["alice", "bob", "charlie", "dom", "elena", "fiona"] self.set_logging_default(False) request.addfinalizer(self.finalize) @@ -399,7 +425,7 @@ class ACFactory: acc.disable_logging() def get_next_liveconfig(self): - """ Base function to get functional online configurations + """Base function to get functional online configurations where we can make valid SMTP and IMAP connections with. """ configdict = next(self._liveconfig_producer).copy() @@ -426,7 +452,9 @@ class ACFactory: # we need to use fixed database basename for maybe_cache_* functions to work path = self.tmpdir.mkdir(logid).join("dc.db") if try_cache_addr: - self.testprocess.cache_maybe_retrieve_configured_db_files(try_cache_addr, path) + self.testprocess.cache_maybe_retrieve_configured_db_files( + try_cache_addr, path + ) ac = Account(path.strpath, logging=self._logging) ac._logid = logid # later instantiated FFIEventLogger needs this ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac)) @@ -448,8 +476,12 @@ class ACFactory: except IndexError: pass else: - fname_pub = self.data.read_path("key/{name}-public.asc".format(name=keyname)) - fname_sec = self.data.read_path("key/{name}-secret.asc".format(name=keyname)) + fname_pub = self.data.read_path( + "key/{name}-public.asc".format(name=keyname) + ) + fname_sec = self.data.read_path( + "key/{name}-secret.asc".format(name=keyname) + ) if fname_pub and fname_sec: account._preconfigure_keypair(addr, fname_pub, fname_sec) return True @@ -461,11 +493,16 @@ class ACFactory: ac = self.get_unconfigured_account() acname = ac._logid addr = "{}@offline.org".format(acname) - ac.update_config(dict( - addr=addr, displayname=acname, mail_pw="123", - configured_addr=addr, configured_mail_pw="123", - configured="1", - )) + ac.update_config( + dict( + addr=addr, + displayname=acname, + mail_pw="123", + configured_addr=addr, + configured_mail_pw="123", + configured="1", + ) + ) self._preconfigure_key(ac, addr) self._acsetup.init_logging(ac) return ac @@ -501,7 +538,7 @@ class ACFactory: return ac def wait_configured(self, account): - """ Wait until the specified account has successfully completed configure. """ + """Wait until the specified account has successfully completed configure.""" self._acsetup.wait_one_configured(account) def bring_accounts_online(self): @@ -531,8 +568,10 @@ class ACFactory: sys.executable, "-u", fn, - "--email", bot_cfg["addr"], - "--password", bot_cfg["mail_pw"], + "--email", + bot_cfg["addr"], + "--password", + bot_cfg["mail_pw"], bot_ac.db_path, ] if ffi: @@ -543,9 +582,9 @@ class ACFactory: stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # combine stdout/stderr in one stream - bufsize=0, # line buffering - close_fds=True, # close all FDs other than 0/1/2 - universal_newlines=True # give back text + bufsize=0, # line buffering + close_fds=True, # close all FDs other than 0/1/2 + universal_newlines=True, # give back text ) bot = BotProcess(popen, addr=bot_cfg["addr"]) self._finalizers.append(bot.kill) @@ -565,7 +604,7 @@ class ACFactory: def introduce_each_other(self, accounts, sending=True): to_wait = [] for i, acc in enumerate(accounts): - for acc2 in accounts[i + 1:]: + for acc2 in accounts[i + 1 :]: chat = self.get_accepted_chat(acc, acc2) if sending: chat.send_text("hi") @@ -599,7 +638,9 @@ class BotProcess: # we read stdout as quickly as we can in a thread and make # 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") + self.stdout_thread = t = threading.Thread( + target=self._run_stdout_thread, name="bot-stdout-thread" + ) t.daemon = True t.start() @@ -622,7 +663,9 @@ class BotProcess: self.popen.wait(timeout=timeout) def fnmatch_lines(self, pattern_lines): - patterns = [x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()] + patterns = [ + x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip() + ] for next_pattern in patterns: print("+++FNMATCH:", next_pattern) ignored = [] diff --git a/python/src/deltachat/tracker.py b/python/src/deltachat/tracker.py index aa5580b86..816b4ca3b 100644 --- a/python/src/deltachat/tracker.py +++ b/python/src/deltachat/tracker.py @@ -1,12 +1,11 @@ - from queue import Queue from threading import Event -from .hookspec import account_hookimpl, Global +from .hookspec import Global, account_hookimpl class ImexFailed(RuntimeError): - """ Exception for signalling that import/export operations failed.""" + """Exception for signalling that import/export operations failed.""" class ImexTracker: @@ -20,18 +19,23 @@ class ImexTracker: elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN": self._imex_events.put(ffi_event.data2) - def wait_progress(self, target_progress, progress_upper_limit=1000, progress_timeout=60): + def wait_progress( + self, target_progress, progress_upper_limit=1000, progress_timeout=60 + ): while True: ev = self._imex_events.get(timeout=progress_timeout) if isinstance(ev, int) and ev >= target_progress: - assert ev <= progress_upper_limit, \ - str(ev) + " exceeded upper progress limit " + str(progress_upper_limit) + assert ev <= progress_upper_limit, ( + str(ev) + + " exceeded upper progress limit " + + str(progress_upper_limit) + ) return ev if ev == 0: return None def wait_finish(self, progress_timeout=60): - """ Return list of written files, raise ValueError if ExportFailed. """ + """Return list of written files, raise ValueError if ExportFailed.""" files_written = [] while True: ev = self._imex_events.get(timeout=progress_timeout) @@ -44,7 +48,7 @@ class ImexTracker: class ConfigureFailed(RuntimeError): - """ Exception for signalling that configuration failed.""" + """Exception for signalling that configuration failed.""" class ConfigureTracker: @@ -77,11 +81,11 @@ class ConfigureTracker: self.account.remove_account_plugin(self) def wait_smtp_connected(self): - """ wait until smtp is configured. """ + """wait until smtp is configured.""" self._smtp_finished.wait() def wait_imap_connected(self): - """ wait until smtp is configured. """ + """wait until smtp is configured.""" self._imap_finished.wait() def wait_progress(self, data1=None): @@ -91,7 +95,7 @@ class ConfigureTracker: break def wait_finish(self, timeout=None): - """ wait until configure is completed. + """wait until configure is completed. Raise Exception if Configure failed """ diff --git a/python/tests/auditwheels.py b/python/tests/auditwheels.py index 4f694fa26..ad0d83ff8 100644 --- a/python/tests/auditwheels.py +++ b/python/tests/auditwheels.py @@ -1,10 +1,8 @@ - import os import platform import subprocess import sys - if __name__ == "__main__": assert len(sys.argv) == 2 workspacedir = sys.argv[1] @@ -13,5 +11,13 @@ if __name__ == "__main__": if relpath.startswith("deltachat"): p = os.path.join(workspacedir, relpath) subprocess.check_call( - ["auditwheel", "repair", p, "-w", workspacedir, - "--plat", "manylinux2014_" + arch]) + [ + "auditwheel", + "repair", + p, + "-w", + workspacedir, + "--plat", + "manylinux2014_" + arch, + ] + ) diff --git a/python/tests/package_wheels.py b/python/tests/package_wheels.py index 1b67fdcb3..90448250f 100644 --- a/python/tests/package_wheels.py +++ b/python/tests/package_wheels.py @@ -1,8 +1,6 @@ - import os -import sys import subprocess - +import sys if __name__ == "__main__": assert len(sys.argv) == 2 @@ -10,6 +8,10 @@ if __name__ == "__main__": # pip wheel will build in an isolated tmp dir that does not have git # history so setuptools_scm can not automatically determine a # version there. So pass in the version through an env var. - version = subprocess.check_output(["python", "setup.py", "--version"]).strip().split(b"\n")[-1] + version = ( + subprocess.check_output(["python", "setup.py", "--version"]) + .strip() + .split(b"\n")[-1] + ) os.environ["SETUPTOOLS_SCM_PRETEND_VERSION"] = version.decode("ascii") subprocess.check_call(("pip wheel . -w %s" % wheelhousedir).split()) diff --git a/python/tests/stress_test_db.py b/python/tests/stress_test_db.py index 9d1849a8c..be26630d0 100644 --- a/python/tests/stress_test_db.py +++ b/python/tests/stress_test_db.py @@ -1,9 +1,9 @@ - -import time -import threading -import pytest import os -from queue import Queue, Empty +import threading +import time +from queue import Empty, Queue + +import pytest import deltachat @@ -24,7 +24,7 @@ def test_db_busy_error(acfactory, tmpdir): for acc in accounts: acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile") with open(acc.bigfile, "wb") as f: - f.write(b"01234567890"*1000_000) + f.write(b"01234567890" * 1000_000) log("created %s bigfiles" % len(accounts)) contact_addrs = [acc.get_self_contact().addr for acc in accounts] @@ -41,7 +41,9 @@ def test_db_busy_error(acfactory, tmpdir): # each replier receives all events and sends report events to receive_queue repliers = [] for acc in accounts: - replier = AutoReplier(acc, log=log, num_send=500, num_bigfiles=5, report_func=report_func) + replier = AutoReplier( + acc, log=log, num_send=500, num_bigfiles=5, report_func=report_func + ) acc.add_account_plugin(replier) repliers.append(replier) @@ -63,9 +65,11 @@ def test_db_busy_error(acfactory, tmpdir): elif report_type == ReportType.message_echo: continue else: - raise ValueError("{} unknown report type {}, args={}".format( - addr, report_type, report_args - )) + raise ValueError( + "{} unknown report type {}, args={}".format( + addr, report_type, report_args + ) + ) alive_count -= 1 replier.log("shutting down") replier.account.shutdown() @@ -89,8 +93,7 @@ class AutoReplier: self.addr = self.account.get_self_contact().addr self._thread = threading.Thread( - name="Stats{}".format(self.account), - target=self.thread_stats + name="Stats{}".format(self.account), target=self.thread_stats ) self._thread.setDaemon(True) self._thread.start() @@ -116,11 +119,16 @@ class AutoReplier: self.current_sent += 1 # we are still alive, let's send a reply - if self.num_bigfiles and self.current_sent % (self.num_send / self.num_bigfiles) == 0: + if ( + self.num_bigfiles + and self.current_sent % (self.num_send / self.num_bigfiles) == 0 + ): message.chat.send_text("send big file as reply to: {}".format(message.text)) msg = message.chat.send_file(self.account.bigfile) else: - msg = message.chat.send_text("got message id {}, small text reply".format(message.id)) + msg = message.chat.send_text( + "got message id {}, small text reply".format(message.id) + ) assert msg.text self.log("message-sent: {}".format(msg)) self.report_func(self, ReportType.message_echo) diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index e1523da06..f30b8a67d 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -1,4 +1,5 @@ import sys + import pytest @@ -215,7 +216,9 @@ def test_fetch_existing(acfactory, lp, mvbox_move): chat.send_text("message text") assert_folders_configured(ac1) - lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen") + lp.sec( + "wait until the bcc_self message arrives in correct folder and is marked seen" + ) assert idle1.wait_for_seen() assert_folders_configured(ac1) @@ -254,7 +257,9 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp): acfactory.bring_accounts_online() lp.sec("receive a message") - ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message") + ac2.create_group_chat("group name", contacts=[ac1]).send_text( + "incoming, unencrypted group message" + ) ac1._evtracker.wait_next_incoming_message() lp.sec("send out message with bcc to ourselves") @@ -277,7 +282,7 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp): assert len(chats) == 4 # two newly created chats + self-chat + device-chat group_chat = [c for c in chats if c.get_name() == "group name"][0] assert group_chat.is_group() - private_chat, = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()] + (private_chat,) = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()] assert not private_chat.is_group() group_messages = group_chat.get_messages() @@ -378,7 +383,7 @@ def test_ephemeral_timer(acfactory, lp): lp.sec("ac1: check that ephemeral timer is set for chat") assert chat1.get_ephemeral_timer() == 60 chat1_summary = chat1.get_summary() - assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}} + assert chat1_summary["ephemeral_timer"] == {"Enabled": {"duration": 60}} lp.sec("ac2: receive system message about ephemeral timer modification") ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED") diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 92d1c86e3..9d59dc8d7 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -1,10 +1,11 @@ import os -import sys import queue +import sys from datetime import datetime, timezone -from imap_tools import AND, U import pytest +from imap_tools import AND, U + from deltachat import const from deltachat.hookspec import account_hookimpl from deltachat.message import Message @@ -34,8 +35,12 @@ def test_basic_imap_api(acfactory, tmpdir): def test_configure_generate_key(acfactory, lp): # A slow test which will generate new keys. acfactory.remove_preconfigured_keys() - ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048)) - ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519)) + ac1 = acfactory.new_online_configuring_account( + key_gen_type=str(const.DC_KEY_GEN_RSA2048) + ) + ac2 = acfactory.new_online_configuring_account( + key_gen_type=str(const.DC_KEY_GEN_ED25519) + ) acfactory.bring_accounts_online() chat = acfactory.get_accepted_chat(ac1, ac2) @@ -78,7 +83,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp): assert len(export_files) == 2 for x in export_files: assert x.startswith(dir.strpath) - key_id, = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*") + (key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*") ac1._evtracker.consume_events() lp.sec("exported keys (private and public)") @@ -86,8 +91,9 @@ def test_export_import_self_keys(acfactory, tmpdir, lp): lp.indent(dir.strpath + os.sep + name) lp.sec("importing into existing account") ac2.import_self_keys(dir.strpath) - key_id2, = ac2._evtracker.get_info_regex_groups( - r".*stored.*KeyId\((.*)\).*", check_error=False) + (key_id2,) = ac2._evtracker.get_info_regex_groups( + r".*stored.*KeyId\((.*)\).*", check_error=False + ) assert key_id2 == key_id @@ -243,7 +249,9 @@ def test_mvbox_sentbox_threads(acfactory, lp): ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True) lp.sec("ac2: start without mvbox/sentbox threads") - ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False) + ac2 = acfactory.new_online_configuring_account( + mvbox_move=False, sentbox_watch=False + ) lp.sec("ac2 and ac1: waiting for configuration") acfactory.bring_accounts_online() @@ -465,7 +473,10 @@ def test_moved_markseen(acfactory, lp): ac2.mark_seen_messages([msg]) uid = idle2.wait_for_seen() - assert len([a for a in ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*")))]) == 1 + assert ( + len([a for a in ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*")))]) + == 1 + ) def test_message_override_sender_name(acfactory, lp): @@ -689,7 +700,7 @@ def test_gossip_encryption_preference(acfactory, lp): msg = ac1._evtracker.wait_next_incoming_message() assert msg.text == "first message" assert not msg.is_encrypted() - res = "End-to-end encryption preferred:\n{}".format(ac2.get_config('addr')) + res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr")) assert msg.chat.get_encryption_info() == res lp.sec("ac2 learns that ac3 prefers encryption") ac2.create_chat(ac3) @@ -701,7 +712,7 @@ def test_gossip_encryption_preference(acfactory, lp): lp.sec("ac3 does not know that ac1 prefers encryption") ac1.create_chat(ac3) chat = ac3.create_chat(ac1) - res = "No encryption:\n{}".format(ac1.get_config('addr')) + res = "No encryption:\n{}".format(ac1.get_config("addr")) assert chat.get_encryption_info() == res msg = chat.send_text("not encrypted") msg = ac1._evtracker.wait_next_incoming_message() @@ -766,7 +777,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp): def test_no_draft_if_cant_send(acfactory): """Tests that no quote can be set if the user can't send to this chat""" - ac1, = acfactory.get_online_accounts(1) + (ac1,) = acfactory.get_online_accounts(1) device_chat = ac1.get_device_chat() msg = Message.new_empty(ac1, "text") device_chat.set_draft(msg) @@ -795,7 +806,9 @@ def test_dont_show_emails(acfactory, lp): acfactory.bring_accounts_online() ac1.stop_io() - ac1.direct_imap.append("Drafts", """ + ac1.direct_imap.append( + "Drafts", + """ From: ac1 <{}> Subject: subj To: alice@example.org @@ -803,8 +816,13 @@ def test_dont_show_emails(acfactory, lp): Content-Type: text/plain; charset=utf-8 message in Drafts that is moved to Sent later - """.format(ac1.get_config("configured_addr"))) - ac1.direct_imap.append("Sent", """ + """.format( + ac1.get_config("configured_addr") + ), + ) + ac1.direct_imap.append( + "Sent", + """ From: ac1 <{}> Subject: subj To: alice@example.org @@ -812,8 +830,13 @@ def test_dont_show_emails(acfactory, lp): Content-Type: text/plain; charset=utf-8 message in Sent - """.format(ac1.get_config("configured_addr"))) - ac1.direct_imap.append("Spam", """ + """.format( + ac1.get_config("configured_addr") + ), + ) + ac1.direct_imap.append( + "Spam", + """ From: unknown.address@junk.org Subject: subj To: {} @@ -821,8 +844,13 @@ def test_dont_show_emails(acfactory, lp): Content-Type: text/plain; charset=utf-8 Unknown message in Spam - """.format(ac1.get_config("configured_addr"))) - ac1.direct_imap.append("Junk", """ + """.format( + ac1.get_config("configured_addr") + ), + ) + ac1.direct_imap.append( + "Junk", + """ From: unknown.address@junk.org Subject: subj To: {} @@ -830,7 +858,10 @@ def test_dont_show_emails(acfactory, lp): Content-Type: text/plain; charset=utf-8 Unknown message in Junk - """.format(ac1.get_config("configured_addr"))) + """.format( + ac1.get_config("configured_addr") + ), + ) ac1.set_config("scan_all_folders_debounce_secs", "0") lp.sec("All prepared, now let DC find the message") @@ -849,7 +880,9 @@ def test_dont_show_emails(acfactory, lp): assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org") ac1.stop_io() - lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time") + lp.sec( + "'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time" + ) ac1.direct_imap.select_folder("Drafts") uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org") ac1.direct_imap.conn.move(uid, "Sent") @@ -884,7 +917,9 @@ def test_no_old_msg_is_fresh(acfactory, lp): assert ac1.create_chat(ac2).count_fresh_messages() == 1 assert len(list(ac1.get_fresh_messages())) == 1 - lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'") + lp.sec( + "Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'" + ) ac1_clone.create_chat(ac2).send_text("Hi back") ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED") @@ -1154,7 +1189,7 @@ def test_send_and_receive_image(acfactory, lp, data): def test_import_export_online_all(acfactory, tmpdir, data, lp): - ac1, = acfactory.get_online_accounts(1) + (ac1,) = acfactory.get_online_accounts(1) lp.sec("create some chat content") chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat() @@ -1180,7 +1215,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp): assert len(messages) == 3 assert messages[0].text == "msg1" assert messages[1].filemime == "image/png" - assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size + assert ( + os.stat(messages[1].filename).st_size + == os.stat(original_image_path).st_size + ) ac.set_config("displayname", "new displayname") assert ac.get_config("displayname") == "new displayname" @@ -1326,7 +1364,9 @@ def test_set_get_contact_avatar(acfactory, data, lp): assert open(received_path, "rb").read() == open(p, "rb").read() lp.sec("ac2: send back message") - msg3 = msg2.create_chat().send_text("yes, i received your avatar -- how do you like mine?") + msg3 = msg2.create_chat().send_text( + "yes, i received your avatar -- how do you like mine?" + ) assert msg3.is_encrypted() lp.sec("ac1: wait for receiving message and avatar from ac2") @@ -1371,11 +1411,17 @@ def test_add_remove_member_remote_events(acfactory, lp): @account_hookimpl def ac_member_added(self, chat, contact, message): - in_list.put(EventHolder(action="added", chat=chat, contact=contact, message=message)) + in_list.put( + EventHolder(action="added", chat=chat, contact=contact, message=message) + ) @account_hookimpl def ac_member_removed(self, chat, contact, message): - in_list.put(EventHolder(action="removed", chat=chat, contact=contact, message=message)) + in_list.put( + EventHolder( + action="removed", chat=chat, contact=contact, message=message + ) + ) ac2.add_account_plugin(InPlugin()) @@ -1387,8 +1433,9 @@ def test_add_remove_member_remote_events(acfactory, lp): ev = in_list.get() assert ev.action == "chat-modified" assert chat.is_promoted() - assert sorted(x.addr for x in chat.get_contacts()) == \ - sorted(x.addr for x in ev.chat.get_contacts()) + assert sorted(x.addr for x in chat.get_contacts()) == sorted( + x.addr for x in ev.chat.get_contacts() + ) lp.sec("ac1: add address2") # note that if the above create_chat() would not @@ -1528,10 +1575,14 @@ def test_connectivity(acfactory, lp): ac1.start_io() ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING) - ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED) + ac1._evtracker.wait_for_connectivity_change( + const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED + ) - lp.sec("Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " + - "all messages are fetched") + lp.sec( + "Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " + + "all messages are fetched" + ) ac1.direct_imap.select_config_folder("inbox") with ac1.direct_imap.idle() as idle1: @@ -1543,18 +1594,26 @@ def test_connectivity(acfactory, lp): assert len(msgs) == 1 assert msgs[0].text == "Hi" - lp.sec("Test that the connectivity changes to WORKING while new messages are fetched") + lp.sec( + "Test that the connectivity changes to WORKING while new messages are fetched" + ) ac2.create_chat(ac1).send_text("Hi 2") - ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING) - ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED) + ac1._evtracker.wait_for_connectivity_change( + const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING + ) + ac1._evtracker.wait_for_connectivity_change( + const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED + ) msgs = ac1.create_chat(ac2).get_messages() assert len(msgs) == 2 assert msgs[1].text == "Hi 2" - lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages") + lp.sec( + "Test that the connectivity doesn't flicker to WORKING if there are no new messages" + ) ac1.maybe_network() while 1: @@ -1563,7 +1622,9 @@ def test_connectivity(acfactory, lp): break ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED") - lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked") + lp.sec( + "Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked" + ) ac1.create_contact(ac2).block() ac1.direct_imap.select_config_folder("inbox") @@ -1594,10 +1655,12 @@ def test_fetch_deleted_msg(acfactory, lp): See https://github.com/deltachat/deltachat-core-rust/issues/2429. """ - ac1, = acfactory.get_online_accounts(1) + (ac1,) = acfactory.get_online_accounts(1) ac1.stop_io() - ac1.direct_imap.append("INBOX", """ + ac1.direct_imap.append( + "INBOX", + """ From: alice Subject: subj To: bob@example.com @@ -1606,7 +1669,8 @@ def test_fetch_deleted_msg(acfactory, lp): Content-Type: text/plain; charset=utf-8 Deleted message - """) + """, + ) ac1.direct_imap.delete("1:*", expunge=False) ac1.start_io() @@ -1626,7 +1690,10 @@ def test_fetch_deleted_msg(acfactory, lp): if ev.name == "DC_EVENT_MSGS_CHANGED": pytest.fail("A deleted message was shown to the user") - if ev.name == "DC_EVENT_INFO" and "INBOX: Idle entering wait-on-remote state" in ev.data2: + if ( + ev.name == "DC_EVENT_INFO" + and "INBOX: Idle entering wait-on-remote state" in ev.data2 + ): break # DC is done with reading messages @@ -1888,11 +1955,26 @@ def test_group_quote(acfactory, lp): assert received_reply.quote.id == out_msg.id -@pytest.mark.parametrize("folder,move,expected_destination,", [ - ("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved - ("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat - ("Spam", False, "INBOX"), # ...emails are moved from the spam folder to the Inbox -]) +@pytest.mark.parametrize( + "folder,move,expected_destination,", + [ + ( + "xyz", + False, + "xyz", + ), # Test that emails are recognized in a random folder but not moved + ( + "xyz", + True, + "DeltaChat", + ), # ...emails are found in a random folder and moved to DeltaChat + ( + "Spam", + False, + "INBOX", + ), # ...emails are moved from the spam folder to the Inbox + ], +) # Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with # the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag. def test_scan_folders(acfactory, lp, folder, move, expected_destination): diff --git a/python/tests/test_2_increation.py b/python/tests/test_2_increation.py index 4046e9ab0..82d493983 100644 --- a/python/tests/test_2_increation.py +++ b/python/tests/test_2_increation.py @@ -2,13 +2,13 @@ from __future__ import print_function import os.path import shutil +from filecmp import cmp import pytest -from filecmp import cmp def wait_msg_delivered(account, msg_list): - """ wait for one or more MSG_DELIVERED events to match msg_list contents. """ + """wait for one or more MSG_DELIVERED events to match msg_list contents.""" msg_list = list(msg_list) while msg_list: ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED") @@ -16,7 +16,7 @@ def wait_msg_delivered(account, msg_list): def wait_msgs_changed(account, msgs_list): - """ wait for one or more MSGS_CHANGED events to match msgs_list contents. """ + """wait for one or more MSGS_CHANGED events to match msgs_list contents.""" account.log("waiting for msgs_list={}".format(msgs_list)) msgs_list = list(msgs_list) while msgs_list: @@ -38,7 +38,7 @@ class TestOnlineInCreation: lp.sec("Creating in-creation file outside of blobdir") assert tmpdir.strpath != ac1.get_blobdir() - src = tmpdir.join('file.txt').ensure(file=1) + src = tmpdir.join("file.txt").ensure(file=1) with pytest.raises(Exception): chat.prepare_message_file(src.strpath) @@ -48,11 +48,11 @@ class TestOnlineInCreation: lp.sec("Creating file outside of blobdir") assert tmpdir.strpath != ac1.get_blobdir() - src = tmpdir.join('file.txt') + src = tmpdir.join("file.txt") src.write("hello there\n") chat.send_file(src.strpath) - blob_src = os.path.join(ac1.get_blobdir(), 'file.txt') + blob_src = os.path.join(ac1.get_blobdir(), "file.txt") assert os.path.exists(blob_src), "file.txt not copied to blobdir" def test_forward_increation(self, acfactory, data, lp): @@ -63,7 +63,7 @@ class TestOnlineInCreation: lp.sec("create a message with a file in creation") orig = data.get_path("d.png") - path = os.path.join(ac1.get_blobdir(), 'd.png') + path = os.path.join(ac1.get_blobdir(), "d.png") with open(path, "x") as fp: fp.write("preparing") prepared_original = chat.prepare_message_file(path) @@ -85,19 +85,22 @@ class TestOnlineInCreation: assert prepared_original.is_out_preparing() shutil.copyfile(orig, path) chat.send_prepared(prepared_original) - assert prepared_original.is_out_pending() or prepared_original.is_out_delivered() + assert ( + prepared_original.is_out_pending() or prepared_original.is_out_delivered() + ) lp.sec("check that both forwarded and original message are proper.") - wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)]) + wait_msgs_changed( + ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)] + ) fwd_msg = ac1.get_message_by_id(forwarded_id) assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered() lp.sec("wait for both messages to be delivered to SMTP") - wait_msg_delivered(ac1, [ - (chat2.id, forwarded_id), - (chat.id, prepared_original.id) - ]) + wait_msg_delivered( + ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)] + ) lp.sec("wait1 for original or forwarded messages to arrive") received_original = ac2._evtracker.wait_next_incoming_message() diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index bde191468..08ae04ef9 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -1,32 +1,50 @@ from __future__ import print_function -import pytest + import os import time -from deltachat import const, Account -from deltachat.message import Message -from deltachat.hookspec import account_hookimpl -from deltachat.capi import ffi, lib -from deltachat.cutil import iter_array from datetime import datetime, timedelta, timezone +import pytest -@pytest.mark.parametrize("msgtext,res", [ - ("Member Me (tmp1@x.org) removed by tmp2@x.org.", - ("removed", "tmp1@x.org", "tmp2@x.org")), - ("Member With space (tmp1@x.org) removed by tmp2@x.org.", - ("removed", "tmp1@x.org", "tmp2@x.org")), - ("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).", - ("removed", "tmp1@x.org", "tmp2@x.org")), - ("Member With space (tmp1@x.org) removed by me", - ("removed", "tmp1@x.org", "me")), - ("Group left by some one (tmp1@x.org).", - ("removed", "tmp1@x.org", "tmp1@x.org")), - ("Group left by tmp1@x.org.", - ("removed", "tmp1@x.org", "tmp1@x.org")), - ("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")), - ("Member nothing bla bla", None), - ("Another unknown system message", None), -]) +from deltachat import Account, const +from deltachat.capi import ffi, lib +from deltachat.cutil import iter_array +from deltachat.hookspec import account_hookimpl +from deltachat.message import Message + + +@pytest.mark.parametrize( + "msgtext,res", + [ + ( + "Member Me (tmp1@x.org) removed by tmp2@x.org.", + ("removed", "tmp1@x.org", "tmp2@x.org"), + ), + ( + "Member With space (tmp1@x.org) removed by tmp2@x.org.", + ("removed", "tmp1@x.org", "tmp2@x.org"), + ), + ( + "Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).", + ("removed", "tmp1@x.org", "tmp2@x.org"), + ), + ( + "Member With space (tmp1@x.org) removed by me", + ("removed", "tmp1@x.org", "me"), + ), + ( + "Group left by some one (tmp1@x.org).", + ("removed", "tmp1@x.org", "tmp1@x.org"), + ), + ("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")), + ( + "Member tmp1@x.org added by tmp2@x.org.", + ("added", "tmp1@x.org", "tmp2@x.org"), + ), + ("Member nothing bla bla", None), + ("Another unknown system message", None), + ], +) def test_parse_system_add_remove(msgtext, res): from deltachat.message import parse_system_add_remove @@ -274,7 +292,11 @@ class TestOfflineChat: assert d["archived"] == chat.is_archived() # assert d["param"] == chat.param assert d["color"] == chat.get_color() - assert d["profile_image"] == "" if chat.get_profile_image() is None else chat.get_profile_image() + assert ( + d["profile_image"] == "" + if chat.get_profile_image() is None + else chat.get_profile_image() + ) assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft() def test_group_chat_creation_with_translation(self, ac1): @@ -424,11 +446,14 @@ class TestOfflineChat: assert os.path.exists(msg.filename) assert msg.filemime == "image/png" - @pytest.mark.parametrize("typein,typeout", [ + @pytest.mark.parametrize( + "typein,typeout", + [ (None, "application/octet-stream"), ("text/plain", "text/plain"), ("image/png", "image/png"), - ]) + ], + ) def test_message_file(self, ac1, chat1, data, lp, typein, typeout): lp.sec("sending file") fn = data.get_path("r.txt") @@ -629,6 +654,6 @@ class TestOfflineChat: lp.sec("check message count of only system messages (without daymarkers)") dc_array = ffi.gc( lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0), - lib.dc_array_unref + lib.dc_array_unref, ) assert len(list(iter_array(dc_array, lambda x: x))) == 2 diff --git a/python/tests/test_4_lowlevel.py b/python/tests/test_4_lowlevel.py index bb3748b24..de671d13f 100644 --- a/python/tests/test_4_lowlevel.py +++ b/python/tests/test_4_lowlevel.py @@ -1,24 +1,25 @@ - import os - from queue import Queue -from deltachat import capi, cutil, const -from deltachat import register_global_plugin + +from deltachat import capi, const, cutil, register_global_plugin +from deltachat.capi import ffi, lib from deltachat.hookspec import global_hookimpl -from deltachat.capi import ffi -from deltachat.capi import lib -from deltachat.testplugin import ACSetup, create_dict_from_files_in_path, write_dict_to_dir +from deltachat.testplugin import ( + ACSetup, + create_dict_from_files_in_path, + write_dict_to_dir, +) + # from deltachat.account import EventLogger class TestACSetup: - def test_cache_writing(self, tmp_path): base = tmp_path.joinpath("hello") base.mkdir() d1 = base.joinpath("dir1") d1.mkdir() - d1.joinpath("file1").write_bytes(b'content1') + d1.joinpath("file1").write_bytes(b"content1") d2 = d1.joinpath("dir2") d2.mkdir() d2.joinpath("file2").write_bytes(b"123") @@ -42,7 +43,9 @@ class TestACSetup: pc.bring_online() assert pc._account2state[acc] == pc.IDLEREADY - def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory, testprocess): + def test_two_accounts_one_waited_all_started( + self, monkeypatch, acfactory, testprocess + ): pc = ACSetup(init_time=0.0, testprocess=testprocess) monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None) monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None) @@ -103,6 +106,7 @@ def test_dc_close_events(tmpdir, acfactory): def dc_account_after_shutdown(self, account): assert account._dc_context is None shutdowns.put(account) + register_global_plugin(ShutdownPlugin()) assert hasattr(ac1, "_dc_context") ac1.shutdown() @@ -168,7 +172,12 @@ def test_provider_info_none(): lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL), lib.dc_context_unref, ) - assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL + assert ( + lib.dc_provider_new_from_email( + ctx, cutil.as_dc_charpointer("email@unexistent.no") + ) + == ffi.NULL + ) def test_get_info_open(tmpdir): @@ -178,8 +187,8 @@ def test_get_info_open(tmpdir): lib.dc_context_unref, ) info = cutil.from_dc_charpointer(lib.dc_get_info(ctx)) - assert 'deltachat_core_version' in info - assert 'database_dir' in info + assert "deltachat_core_version" in info + assert "database_dir" in info def test_logged_hook_failure(acfactory): @@ -187,7 +196,7 @@ def test_logged_hook_failure(acfactory): cap = [] ac1.log = cap.append with ac1._event_thread.swallow_and_log_exception("some"): - 0/0 + 0 / 0 assert cap assert "some" in str(cap) assert "ZeroDivisionError" in str(cap) @@ -202,7 +211,7 @@ def test_logged_ac_process_ffi_failure(acfactory): class FailPlugin: @account_hookimpl def ac_process_ffi_event(ffi_event): - 0/0 + 0 / 0 cap = Queue() ac1.log = cap.put diff --git a/python/tox.ini b/python/tox.ini index aa5e9dc60..4cadb72e6 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -36,10 +36,14 @@ skipsdist = True skip_install = True deps = flake8 + isort + black # pygments required by rst-lint pygments restructuredtext_lint commands = + isort --check --profile black src/deltachat examples/ tests/ + black --check src/deltachat examples/ tests/ flake8 src/deltachat flake8 tests/ examples/ rst-lint --encoding 'utf-8' README.rst