Merge pull request #3381 from deltachat/adb/apply-black-and-isort

apply isort and black formatters, add format checking to CI
This commit is contained in:
Asiel Díaz Benítez
2022-06-02 08:01:17 -04:00
committed by GitHub
27 changed files with 674 additions and 549 deletions

View File

@@ -1,4 +1,3 @@
# content of echo_and_quit.py # content of echo_and_quit.py
from deltachat import account_hookimpl, run_cmdline from deltachat import account_hookimpl, run_cmdline

View File

@@ -1,4 +1,3 @@
# content of group_tracking.py # content of group_tracking.py
from deltachat import account_hookimpl, run_cmdline from deltachat import account_hookimpl, run_cmdline
@@ -33,15 +32,21 @@ class GroupTrackingPlugin:
@account_hookimpl @account_hookimpl
def ac_member_added(self, chat, contact, actor, message): def ac_member_added(self, chat, contact, actor, message):
print("ac_member_added {} to chat {} from {}".format( print(
contact.addr, chat.id, actor or message.get_sender_contact().addr)) "ac_member_added {} to chat {} from {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr
)
)
for member in chat.get_contacts(): for member in chat.get_contacts():
print("chat member: {}".format(member.addr)) print("chat member: {}".format(member.addr))
@account_hookimpl @account_hookimpl
def ac_member_removed(self, chat, contact, actor, message): def ac_member_removed(self, chat, contact, actor, message):
print("ac_member_removed {} from chat {} by {}".format( print(
contact.addr, chat.id, actor or message.get_sender_contact().addr)) "ac_member_removed {} from chat {} by {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr
)
)
def main(argv=None): def main(argv=None):

View File

@@ -1,20 +1,20 @@
import pytest
import py
import echo_and_quit import echo_and_quit
import group_tracking import group_tracking
import py
import pytest
from deltachat.events import FFIEventLogger from deltachat.events import FFIEventLogger
@pytest.fixture(scope='session') @pytest.fixture(scope="session")
def datadir(): def datadir():
"""The py.path.local object of the test-data/ directory.""" """The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()): for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data') datadir = path.join("test-data")
if datadir.isdir(): if datadir.isdir():
return datadir return datadir
else: else:
pytest.skip('test-data directory not found') pytest.skip("test-data directory not found")
def test_echo_quit_plugin(acfactory, lp): 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) botproc = acfactory.run_bot_process(echo_and_quit)
lp.sec("creating a temp account to contact the bot") 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") lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr) 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) ac1, ac2 = acfactory.get_online_accounts(2)
botproc.fnmatch_lines(""" botproc.fnmatch_lines(
"""
*ac_configure_completed* *ac_configure_completed*
""") """
)
ac1.add_account_plugin(FFIEventLogger(ac1)) ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2)) ac2.add_account_plugin(FFIEventLogger(ac2))
@@ -56,9 +58,11 @@ def test_group_tracking_plugin(acfactory, lp):
ch.add_contact(bot_contact) ch.add_contact(bot_contact)
ch.send_text("hello") ch.send_text("hello")
botproc.fnmatch_lines(""" botproc.fnmatch_lines(
"""
*ac_chat_modified*bot test group* *ac_chat_modified*bot test group*
""") """
)
lp.sec("adding third member {}".format(ac2.get_config("addr"))) lp.sec("adding third member {}".format(ac2.get_config("addr")))
contact3 = ac1.create_contact(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 assert "hello" in reply.text
lp.sec("now looking at what the bot received") lp.sec("now looking at what the bot received")
botproc.fnmatch_lines(""" botproc.fnmatch_lines(
"""
*ac_member_added {}*from*{}* *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") lp.sec("contact successfully added, now removing")
ch.remove_contact(contact3) ch.remove_contact(contact3)
botproc.fnmatch_lines(""" botproc.fnmatch_lines(
"""
*ac_member_removed {}*from*{}* *ac_member_removed {}*from*{}*
""".format(contact3.addr, ac1.get_config("addr"))) """.format(
contact3.addr, ac1.get_config("addr")
)
)

View File

@@ -6,3 +6,6 @@ build-backend = "setuptools.build_meta"
root = ".." root = ".."
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$' tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
git_describe_command = "git describe --dirty --tags --long --match py-*.*" git_describe_command = "git describe --dirty --tags --long --match py-*.*"
[tool.black]
line-length = 120

View File

@@ -1,15 +1,15 @@
import sys import sys
from . import capi, const, hookspec # noqa from pkg_resources import DistributionNotFound, get_distribution
from .capi import ffi # noqa
from .account import Account, get_core_info # noqa from . import capi, const, events, hookspec # noqa
from .message import Message # noqa from .account import Account, get_core_info # noqa
from .contact import Contact # noqa from .capi import ffi # noqa
from .chat import Chat # noqa from .chat import Chat # noqa
from .hookspec import account_hookimpl, global_hookimpl # noqa from .contact import Contact # noqa
from . import events from .hookspec import account_hookimpl, global_hookimpl # noqa
from .message import Message # noqa
from pkg_resources import get_distribution, DistributionNotFound
try: try:
__version__ = get_distribution(__name__).version __version__ = get_distribution(__name__).version
except DistributionNotFound: except DistributionNotFound:
@@ -46,6 +46,7 @@ def run_cmdline(argv=None, account_plugins=None):
"""Run a simple default command line app, registering the specified """Run a simple default command line app, registering the specified
account plugins.""" account plugins."""
import argparse import argparse
if argv is None: if argv is None:
argv = sys.argv argv = sys.argv
@@ -69,9 +70,9 @@ def run_cmdline(argv=None, account_plugins=None):
ac.add_account_plugin(plugin) ac.add_account_plugin(plugin)
if not ac.is_configured(): if not ac.is_configured():
assert args.email and args.password, ( assert (
"you must specify --email and --password once to configure this database/account" 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("addr", args.email)
ac.set_config("mail_pw", args.password) ac.set_config("mail_pw", args.password)
ac.set_config("mvbox_move", "0") ac.set_config("mvbox_move", "0")

View File

@@ -20,30 +20,33 @@ def local_build_flags(projdir, target):
:param target: The rust build target, `debug` or `release`. :param target: The rust build target, `debug` or `release`.
""" """
flags = {} flags = {}
if platform.system() == 'Darwin': if platform.system() == "Darwin":
flags['libraries'] = ['resolv', 'dl'] flags["libraries"] = ["resolv", "dl"]
flags['extra_link_args'] = [ flags["extra_link_args"] = [
'-framework', 'CoreFoundation', "-framework",
'-framework', 'CoreServices', "CoreFoundation",
'-framework', 'Security', "-framework",
"CoreServices",
"-framework",
"Security",
] ]
elif platform.system() == 'Linux': elif platform.system() == "Linux":
flags['libraries'] = ['rt', 'dl', 'm'] flags["libraries"] = ["rt", "dl", "m"]
flags['extra_link_args'] = [] flags["extra_link_args"] = []
else: 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") target_dir = os.environ.get("CARGO_TARGET_DIR")
if target_dir is None: if target_dir is None:
target_dir = os.path.join(projdir, 'target') target_dir = os.path.join(projdir, "target")
flags['extra_objects'] = [os.path.join(target_dir, target, 'libdeltachat.a')] flags["extra_objects"] = [os.path.join(target_dir, target, "libdeltachat.a")]
assert os.path.exists(flags['extra_objects'][0]), flags['extra_objects'] assert os.path.exists(flags["extra_objects"][0]), flags["extra_objects"]
flags['include_dirs'] = [os.path.join(projdir, 'deltachat-ffi')] flags["include_dirs"] = [os.path.join(projdir, "deltachat-ffi")]
return flags return flags
def system_build_flags(): def system_build_flags():
"""Construct build flags for building against an installed libdeltachat.""" """Construct build flags for building against an installed libdeltachat."""
return pkgconfig.parse('deltachat') return pkgconfig.parse("deltachat")
def extract_functions(flags): def extract_functions(flags):
@@ -61,11 +64,13 @@ def extract_functions(flags):
src_name = os.path.join(tmpdir, "include.h") src_name = os.path.join(tmpdir, "include.h")
dst_name = os.path.join(tmpdir, "expanded.h") dst_name = os.path.join(tmpdir, "expanded.h")
with open(src_name, "w") as src_fp: with open(src_name, "w") as src_fp:
src_fp.write('#include <deltachat.h>') src_fp.write("#include <deltachat.h>")
cc.preprocess(source=src_name, cc.preprocess(
source=src_name,
output_file=dst_name, output_file=dst_name,
include_dirs=flags['include_dirs'], include_dirs=flags["include_dirs"],
macros=[('PY_CFFI', '1')]) macros=[("PY_CFFI", "1")],
)
with open(dst_name, "r") as dst_fp: with open(dst_name, "r") as dst_fp:
return dst_fp.read() return dst_fp.read()
finally: finally:
@@ -87,7 +92,9 @@ def find_header(flags):
obj_name = os.path.join(tmpdir, "where.o") obj_name = os.path.join(tmpdir, "where.o")
dst_name = os.path.join(tmpdir, "where") dst_name = os.path.join(tmpdir, "where")
with open(src_name, "w") as src_fp: with open(src_name, "w") as src_fp:
src_fp.write(textwrap.dedent(""" src_fp.write(
textwrap.dedent(
"""
#include <stdio.h> #include <stdio.h>
#include <deltachat.h> #include <deltachat.h>
@@ -95,18 +102,20 @@ def find_header(flags):
printf("%s", _dc_header_file_location()); printf("%s", _dc_header_file_location());
return 0; return 0;
} }
""")) """
)
)
cwd = os.getcwd() cwd = os.getcwd()
try: try:
os.chdir(tmpdir) os.chdir(tmpdir)
cc.compile(sources=["where.c"], cc.compile(
include_dirs=flags['include_dirs'], sources=["where.c"],
macros=[("PY_CFFI_INC", "1")]) include_dirs=flags["include_dirs"],
macros=[("PY_CFFI_INC", "1")],
)
finally: finally:
os.chdir(cwd) os.chdir(cwd)
cc.link_executable(objects=[obj_name], cc.link_executable(objects=[obj_name], output_progname="where", output_dir=tmpdir)
output_progname="where",
output_dir=tmpdir)
return subprocess.check_output(dst_name) return subprocess.check_output(dst_name)
finally: finally:
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
@@ -123,7 +132,8 @@ def extract_defines(flags):
cdef() method. cdef() method.
""" """
header = find_header(flags) header = find_header(flags)
defines_re = re.compile(r""" defines_re = re.compile(
r"""
\#define\s+ # The start of a define. \#define\s+ # The start of a define.
( # Begin capturing group which captures the define name. ( # Begin capturing group which captures the define name.
(?: # A nested group which is not captured, this allows us (?: # A nested group which is not captured, this allows us
@@ -151,26 +161,28 @@ def extract_defines(flags):
) # Close the capturing group, this contains ) # Close the capturing group, this contains
# the entire name e.g. DC_MSG_TEXT. # the entire name e.g. DC_MSG_TEXT.
\s+\S+ # Ensure there is whitespace followed by a value. \s+\S+ # Ensure there is whitespace followed by a value.
""", re.VERBOSE) """,
re.VERBOSE,
)
defines = [] defines = []
with open(header) as fp: with open(header) as fp:
for line in fp: for line in fp:
match = defines_re.match(line) match = defines_re.match(line)
if match: if match:
defines.append(match.group(1)) 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(): def ffibuilder():
projdir = os.environ.get('DCC_RS_DEV') projdir = os.environ.get("DCC_RS_DEV")
if projdir: if projdir:
target = os.environ.get('DCC_RS_TARGET', 'release') target = os.environ.get("DCC_RS_TARGET", "release")
flags = local_build_flags(projdir, target) flags = local_build_flags(projdir, target)
else: else:
flags = system_build_flags() flags = system_build_flags()
builder = cffi.FFI() builder = cffi.FFI()
builder.set_source( builder.set_source(
'deltachat.capi', "deltachat.capi",
""" """
#include <deltachat.h> #include <deltachat.h>
int dc_event_has_string_data(int e) int dc_event_has_string_data(int e)
@@ -180,11 +192,13 @@ def ffibuilder():
""", """,
**flags, **flags,
) )
builder.cdef(""" builder.cdef(
"""
typedef int... time_t; typedef int... time_t;
void free(void *ptr); void free(void *ptr);
extern int dc_event_has_string_data(int); extern int dc_event_has_string_data(int);
""") """
)
function_defs = extract_functions(flags) function_defs = extract_functions(flags)
defines = extract_defines(flags) defines = extract_defines(flags)
builder.cdef(function_defs) builder.cdef(function_defs)
@@ -192,8 +206,9 @@ def ffibuilder():
return builder return builder
if __name__ == '__main__': if __name__ == "__main__":
import os.path import os.path
pkgdir = os.path.join(os.path.dirname(__file__), '..')
pkgdir = os.path.join(os.path.dirname(__file__), "..")
builder = ffibuilder() builder = ffibuilder()
builder.compile(tmpdir=pkgdir, verbose=True) builder.compile(tmpdir=pkgdir, verbose=True)

View File

@@ -1,21 +1,28 @@
""" Account class implementation. """ """ Account class implementation. """
from __future__ import print_function from __future__ import print_function
import os
from array import array
from contextlib import contextmanager from contextlib import contextmanager
from email.utils import parseaddr from email.utils import parseaddr
from threading import Event from threading import Event
import os from typing import Any, Dict, Generator, List, Optional, Union
from array import array
from . import const from . import const, hookspec
from .capi import ffi, lib 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 .chat import Chat
from .message import Message
from .contact import Contact from .contact import Contact
from .tracker import ImexTracker, ConfigureTracker from .cutil import (
from . import hookspec DCLot,
as_dc_charpointer,
from_dc_charpointer,
from_optional_dc_charpointer,
iter_array,
)
from .events import EventThread 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): class MissingCredentials(ValueError):
@@ -28,10 +35,12 @@ def get_core_info():
with NamedTemporaryFile() as path: with NamedTemporaryFile() as path:
path.close() path.close()
return get_dc_info_as_dict(ffi.gc( 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_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
lib.dc_context_unref, lib.dc_context_unref,
)) )
)
def get_dc_info_as_dict(dc_context): def get_dc_info_as_dict(dc_context):
@@ -50,6 +59,7 @@ class Account(object):
by the underlying deltachat core library. All public Account methods are by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects. meant to be memory-safe and return memory-safe objects.
""" """
MissingCredentials = MissingCredentials MissingCredentials = MissingCredentials
def __init__(self, db_path, os_name=None, logging=True) -> None: def __init__(self, db_path, os_name=None, logging=True) -> None:
@@ -102,8 +112,7 @@ class Account(object):
def _check_config_key(self, name: str) -> None: def _check_config_key(self, name: str) -> None:
if name not in self._configkeys: if name not in self._configkeys:
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format( raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(name, self._configkeys))
name, self._configkeys))
def get_info(self) -> Dict[str, str]: def get_info(self) -> Dict[str, str]:
"""return dictionary of built config parameters.""" """return dictionary of built config parameters."""
@@ -175,10 +184,12 @@ class Account(object):
In other words, you don't need this. In other words, you don't need this.
""" """
res = lib.dc_preconfigure_keypair(self._dc_context, res = lib.dc_preconfigure_keypair(
self._dc_context,
as_dc_charpointer(addr), as_dc_charpointer(addr),
as_dc_charpointer(public), as_dc_charpointer(public),
as_dc_charpointer(secret)) as_dc_charpointer(secret),
)
if res == 0: if res == 0:
raise Exception("Failed to set key") raise Exception("Failed to set key")
@@ -224,8 +235,7 @@ class Account(object):
raise ValueError("need to configure first") raise ValueError("need to configure first")
def get_latest_backupfile(self, backupdir) -> Optional[str]: def get_latest_backupfile(self, backupdir) -> Optional[str]:
""" return the latest backup file in a given directory. """return the latest backup file in a given directory."""
"""
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir)) res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
return from_optional_dc_charpointer(res) return from_optional_dc_charpointer(res)
@@ -318,10 +328,7 @@ class Account(object):
:returns: list of :class:`deltachat.contact.Contact` objects. :returns: list of :class:`deltachat.contact.Contact` objects.
""" """
dc_array = ffi.gc( 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))) return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts( def get_contacts(
@@ -344,18 +351,12 @@ class Account(object):
flags |= const.DC_GCL_VERIFIED_ONLY flags |= const.DC_GCL_VERIFIED_ONLY
if with_self: if with_self:
flags |= const.DC_GCL_ADD_SELF flags |= const.DC_GCL_ADD_SELF
dc_array = ffi.gc( 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))) return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self) -> Generator[Message, None, None]: def get_fresh_messages(self) -> Generator[Message, None, None]:
"""yield all fresh messages from all chats.""" """yield all fresh messages from all chats."""
dc_array = ffi.gc( dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
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)) yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat(self, obj) -> Chat: def create_chat(self, obj) -> Chat:
@@ -389,10 +390,7 @@ class Account(object):
:returns: a list of :class:`deltachat.chat.Chat` objects. :returns: a list of :class:`deltachat.chat.Chat` objects.
""" """
dc_chatlist = ffi.gc( 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 assert dc_chatlist != ffi.NULL
chatlist = [] chatlist = []
@@ -528,10 +526,7 @@ class Account(object):
def check_qr(self, qr): 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( 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) lot = DCLot(res)
if lot.state() == const.DC_QR_ERROR: if lot.state() == const.DC_QR_ERROR:
raise ValueError("invalid or unknown QR code: {}".format(lot.text1())) raise ValueError("invalid or unknown QR code: {}".format(lot.text1()))
@@ -566,9 +561,7 @@ class Account(object):
raise ValueError("could not join group") raise ValueError("could not join group")
return Chat(self, chat_id) return Chat(self, chat_id)
def set_location( def set_location(self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0) -> None:
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
) -> None:
"""set a new location. It effects all chats where we currently """set a new location. It effects all chats where we currently
have enabled location streaming. have enabled location streaming.

View File

@@ -1,16 +1,22 @@
""" Chat and Location related API. """ """ Chat and Location related API. """
import mimetypes
import calendar import calendar
import json import json
from datetime import datetime, timezone import mimetypes
import os import os
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array from datetime import datetime, timezone
from .capi import lib, ffi
from . import const
from .message import Message
from typing import Optional 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): 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.
@@ -20,13 +26,13 @@ class Chat(object):
def __init__(self, account, id) -> None: def __init__(self, account, id) -> None:
from .account import Account from .account import Account
assert isinstance(account, Account), repr(account) assert isinstance(account, Account), repr(account)
self.account = account self.account = account
self.id = id self.id = id
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
return self.id == getattr(other, "id", None) and \ return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context
self.account._dc_context == other.account._dc_context
def __ne__(self, other) -> bool: def __ne__(self, other) -> bool:
return not (self == other) return not (self == other)
@@ -36,10 +42,7 @@ class Chat(object):
@property @property
def _dc_chat(self): def _dc_chat(self):
return ffi.gc( 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: def delete(self) -> None:
"""Delete this chat and all its messages. """Delete this chat and all its messages.
@@ -343,7 +346,7 @@ class Chat(object):
""" """
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0), 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))) return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
@@ -399,19 +402,18 @@ class Chat(object):
:returns: list of :class:`deltachat.contact.Contact` objects for this chat :returns: list of :class:`deltachat.contact.Contact` objects for this chat
""" """
from .contact import Contact from .contact import Contact
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_contacts(self.account._dc_context, self.id), lib.dc_get_chat_contacts(self.account._dc_context, self.id),
lib.dc_array_unref lib.dc_array_unref,
)
return list(iter_array(
dc_array, lambda id: Contact(self.account, id))
) )
return list(iter_array(dc_array, lambda id: Contact(self.account, id)))
def num_contacts(self): def num_contacts(self):
"""return number of contacts in this chat.""" """return number of contacts in this chat."""
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_contacts(self.account._dc_context, self.id), 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) return lib.dc_array_get_cnt(dc_array)
@@ -513,10 +515,7 @@ class Chat(object):
latitude=lib.dc_array_get_latitude(dc_array, i), latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i), longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i), accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.fromtimestamp( 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)) for i in range(lib.dc_array_get_cnt(dc_array))

View File

@@ -14,8 +14,10 @@ class Contact(object):
You obtain instances of it through :class:`deltachat.account.Account`. You obtain instances of it through :class:`deltachat.account.Account`.
""" """
def __init__(self, account, id): def __init__(self, account, id):
from .account import Account from .account import Account
assert isinstance(account, Account), repr(account) assert isinstance(account, Account), repr(account)
self.account = account self.account = account
self.id = id self.id = id
@@ -31,10 +33,7 @@ class Contact(object):
@property @property
def _dc_contact(self): def _dc_contact(self):
return ffi.gc( 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 @props.with_doc
def addr(self) -> str: def addr(self) -> str:
@@ -52,9 +51,7 @@ class Contact(object):
@props.with_doc @props.with_doc
def last_seen(self) -> date: def last_seen(self) -> date:
"""Last seen timestamp.""" """Last seen timestamp."""
return datetime.fromtimestamp( return datetime.fromtimestamp(lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc)
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
)
def is_blocked(self): def is_blocked(self):
"""Return True if the contact is blocked.""" """Return True if the contact is blocked."""

View File

@@ -1,9 +1,9 @@
from .capi import lib
from .capi import ffi
from datetime import datetime, timezone 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): def as_dc_charpointer(obj):

View File

@@ -3,18 +3,27 @@ Internal Python-level IMAP handling used by the testplugin
and for cleaning up inbox/mvbox for each test function run. 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 import imaplib
from deltachat import const, Account import io
import pathlib
import ssl
from contextlib import contextmanager
from typing import List from typing import List
from imap_tools import (
AND,
Header,
MailBox,
MailBoxTls,
MailMessage,
MailMessageFlags,
errors,
)
FLAGS = b'FLAGS' from deltachat import Account, const
FETCH = b'FETCH'
FLAGS = b"FLAGS"
FETCH = b"FETCH"
ALL = "1:*" ALL = "1:*"
@@ -88,7 +97,7 @@ class DirectImap:
to make sure the messages are really gone and not to make sure the messages are really gone and not
just flagged as deleted. 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: if expunge:
self.conn.expunge() self.conn.expunge()
@@ -141,7 +150,13 @@ class DirectImap:
fn = path.joinpath(str(msg.uid)) fn = path.joinpath(str(msg.uid))
fn.write_bytes(body) fn.write_bytes(body)
log("Message", msg.uid, fn) 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: if empty_folders:
log("--------- EMPTY FOLDERS:", empty_folders) log("--------- EMPTY FOLDERS:", empty_folders)
@@ -163,11 +178,11 @@ class DirectImap:
""" """
if msg.startswith("\n"): if msg.startswith("\n"):
msg = msg[1:] msg = msg[1:]
msg = '\n'.join([s.lstrip() for s in msg.splitlines()]) msg = "\n".join([s.lstrip() for s in msg.splitlines()])
self.conn.append(bytes(msg, encoding='ascii'), folder) self.conn.append(bytes(msg, encoding="ascii"), folder)
def get_uid_by_message_id(self, message_id) -> str: 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: 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] return msgs[0]
@@ -192,18 +207,17 @@ class IdleManager:
def wait_for_new_message(self, timeout=None) -> bytes: def wait_for_new_message(self, timeout=None) -> bytes:
while 1: while 1:
for item in self.check(timeout=timeout): 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 return item
def wait_for_seen(self, timeout=None) -> int: 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: while 1:
for item in self.check(timeout=timeout): for item in self.check(timeout=timeout):
if FETCH in item: if FETCH in item:
self.log(str(item)) self.log(str(item))
if FLAGS in item and rb'\Seen' in item: if FLAGS in item and rb"\Seen" in item:
return int(item.split(b' ')[1]) return int(item.split(b" ")[1])
def done(self): 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."""

View File

@@ -1,18 +1,19 @@
import threading
import sys
import traceback
import time
import io import io
import re
import os 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 import deltachat
from .hookspec import account_hookimpl
from contextlib import contextmanager
from .capi import ffi, lib from .capi import ffi, lib
from .message import map_system_message
from .cutil import from_optional_dc_charpointer from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
class FFIEvent: class FFIEvent:
@@ -29,6 +30,7 @@ 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. you'll get all ffi-events printed.
""" """
# to prevent garbled logging # to prevent garbled logging
_loglock = threading.RLock() _loglock = threading.RLock()
@@ -56,9 +58,9 @@ class FFIEventLogger:
s = "{:2.2f} [{}] {}".format(elapsed, locname, message) s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
if os.name == "posix": if os.name == "posix":
WARN = '\033[93m' WARN = "\033[93m"
ERROR = '\033[91m' ERROR = "\033[91m"
ENDC = '\033[0m' ENDC = "\033[0m"
if message.startswith("DC_EVENT_WARNING"): if message.startswith("DC_EVENT_WARNING"):
s = WARN + s + ENDC s = WARN + s + ENDC
if message.startswith("DC_EVENT_ERROR"): if message.startswith("DC_EVENT_ERROR"):
@@ -195,6 +197,7 @@ class EventThread(threading.Thread):
With each Account init this callback thread is started. With each Account init this callback thread is started.
""" """
def __init__(self, account) -> None: def __init__(self, account) -> None:
self.account = account self.account = account
super(EventThread, self).__init__(name="events") super(EventThread, self).__init__(name="events")
@@ -259,8 +262,7 @@ class EventThread(threading.Thread):
except Exception as ex: except Exception as ex:
logfile = io.StringIO() logfile = io.StringIO()
traceback.print_exception(*sys.exc_info(), file=logfile) traceback.print_exception(*sys.exc_info(), file=logfile)
self.account.log("{}\nException {}\nTraceback:\n{}" self.account.log("{}\nException {}\nTraceback:\n{}".format(info, ex, logfile.getvalue()))
.format(info, ex, logfile.getvalue()))
def _map_ffi_event(self, ffi_event: FFIEvent): def _map_ffi_event(self, ffi_event: FFIEvent):
name = ffi_event.name name = ffi_event.name
@@ -282,7 +284,10 @@ class EventThread(threading.Thread):
yield res yield res
yield "ac_outgoing_message", dict(message=msg) yield "ac_outgoing_message", dict(message=msg)
elif msg.is_in_fresh(): 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": elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2) msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg) yield "ac_message_delivered", dict(message=msg)

View File

@@ -2,7 +2,6 @@
import pluggy import pluggy
account_spec_name = "deltachat-account" account_spec_name = "deltachat-account"
account_hookspec = pluggy.HookspecMarker(account_spec_name) account_hookspec = pluggy.HookspecMarker(account_spec_name)
account_hookimpl = pluggy.HookimplMarker(account_spec_name) account_hookimpl = pluggy.HookimplMarker(account_spec_name)
@@ -19,6 +18,7 @@ class PerAccount:
Hooks are generally not allowed to block/last long as this Hooks are generally not allowed to block/last long as this
blocks overall event processing on the python side. blocks overall event processing on the python side.
""" """
@classmethod @classmethod
def _make_plugin_manager(cls): def _make_plugin_manager(cls):
pm = pluggy.PluginManager(account_spec_name) pm = pluggy.PluginManager(account_spec_name)
@@ -89,6 +89,7 @@ class Global:
plugin manager instance. plugin manager instance.
""" """
_plugin_manager = None _plugin_manager = None
@classmethod @classmethod

View File

@@ -2,13 +2,13 @@
import os import os
import re 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 datetime import datetime, timezone
from typing import Optional 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): class Message(object):
"""Message object. """Message object.
@@ -16,6 +16,7 @@ class Message(object):
You obtain instances of it through :class:`deltachat.account.Account` or You obtain instances of it through :class:`deltachat.account.Account` or
:class:`deltachat.chat.Chat`. :class:`deltachat.chat.Chat`.
""" """
def __init__(self, account, dc_msg): def __init__(self, account, dc_msg):
self.account = account self.account = account
assert isinstance(self.account._dc_context, ffi.CData) assert isinstance(self.account._dc_context, ffi.CData)
@@ -32,16 +33,20 @@ class Message(object):
c = self.get_sender_contact() c = self.get_sender_contact()
typ = "outgoing" if self.is_outgoing() else "incoming" typ = "outgoing" if self.is_outgoing() else "incoming"
return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format( return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format(
typ, self.is_system_message(), repr(self.text[:10]), typ,
self.id, c.id, c.addr, self.chat.id, self.chat.get_name()) self.is_system_message(),
repr(self.text[:10]),
self.id,
c.id,
c.addr,
self.chat.id,
self.chat.get_name(),
)
@classmethod @classmethod
def from_db(cls, account, id): def from_db(cls, account, id):
assert id > 0 assert id > 0
return cls(account, ffi.gc( return cls(account, ffi.gc(lib.dc_get_msg(account._dc_context, id), lib.dc_msg_unref))
lib.dc_get_msg(account._dc_context, id),
lib.dc_msg_unref
))
@classmethod @classmethod
def new_empty(cls, account, view_type): def new_empty(cls, account, view_type):
@@ -54,10 +59,10 @@ class Message(object):
view_type_code = view_type view_type_code = view_type
else: else:
view_type_code = get_viewtype_code_from_name(view_type) view_type_code = get_viewtype_code_from_name(view_type)
return Message(account, ffi.gc( return Message(
lib.dc_msg_new(account._dc_context, view_type_code), account,
lib.dc_msg_unref ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref),
)) )
def create_chat(self): 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.
@@ -87,8 +92,7 @@ class Message(object):
@props.with_doc @props.with_doc
def html(self) -> str: def html(self) -> str:
"""html text of this messages (might be empty if not an html message).""" """html text of this messages (might be empty if not an html message)."""
return from_optional_dc_charpointer( return from_optional_dc_charpointer(lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
def has_html(self): def has_html(self):
"""return True if this message has an html part, False otherwise.""" """return True if this message has an html part, False otherwise."""
@@ -157,11 +161,7 @@ class Message(object):
def continue_key_transfer(self, setup_code): 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( 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: if res == 0:
raise ValueError("could not decrypt") raise ValueError("could not decrypt")
@@ -238,6 +238,7 @@ class Message(object):
:returns: email-mime message object (with headers only, no body). :returns: email-mime message object (with headers only, no body).
""" """
import email.parser import email.parser
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id) mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
if mime_headers: if mime_headers:
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref)) s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
@@ -257,6 +258,7 @@ class Message(object):
:returns: :class:`deltachat.chat.Chat` object :returns: :class:`deltachat.chat.Chat` object
""" """
from .chat import Chat from .chat import Chat
chat_id = lib.dc_msg_get_chat_id(self._dc_msg) chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self.account, chat_id) return Chat(self.account, chat_id)
@@ -266,13 +268,11 @@ class Message(object):
Usually used to impersonate someone else. Usually used to impersonate someone else.
""" """
return from_optional_dc_charpointer( 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): def set_override_sender_name(self, name):
"""set different sender name for a message.""" """set different sender name for a message."""
lib.dc_msg_set_override_sender_name( lib.dc_msg_set_override_sender_name(self._dc_msg, as_dc_charpointer(name))
self._dc_msg, as_dc_charpointer(name))
def get_sender_chat(self): def get_sender_chat(self):
"""return the 1:1 chat with the sender of this message. """return the 1:1 chat with the sender of this message.
@@ -287,6 +287,7 @@ class Message(object):
:returns: :class:`deltachat.chat.Contact` instance :returns: :class:`deltachat.chat.Contact` instance
""" """
from .contact import Contact from .contact import Contact
contact_id = lib.dc_msg_get_from_id(self._dc_msg) contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self.account, contact_id) return Contact(self.account, contact_id)
@@ -299,10 +300,7 @@ class Message(object):
dc_msg = self._dc_msg dc_msg = self._dc_msg
else: else:
# load message from db to get a fresh/current state # load message from db to get a fresh/current state
dc_msg = ffi.gc( 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) return lib.dc_msg_get_state(dc_msg)
def is_in_fresh(self): def is_in_fresh(self):
@@ -332,23 +330,23 @@ class Message(object):
def is_outgoing(self): def is_outgoing(self):
"""Return True if Message is outgoing.""" """Return True if Message is outgoing."""
return self._msgstate in ( return self._msgstate in (
const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING, const.DC_STATE_OUT_PREPARING,
const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD, const.DC_STATE_OUT_PENDING,
const.DC_STATE_OUT_DELIVERED) const.DC_STATE_OUT_FAILED,
const.DC_STATE_OUT_MDN_RCVD,
const.DC_STATE_OUT_DELIVERED,
)
def is_out_preparing(self): 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 return self._msgstate == const.DC_STATE_OUT_PREPARING
def is_out_pending(self): 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 return self._msgstate == const.DC_STATE_OUT_PENDING
def is_out_failed(self): 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 return self._msgstate == const.DC_STATE_OUT_FAILED
def is_out_delivered(self): def is_out_delivered(self):
@@ -410,13 +408,13 @@ class Message(object):
# some code for handling DC_MSG_* view types # some code for handling DC_MSG_* view types
_view_type_mapping = { _view_type_mapping = {
'text': const.DC_MSG_TEXT, "text": const.DC_MSG_TEXT,
'image': const.DC_MSG_IMAGE, "image": const.DC_MSG_IMAGE,
'gif': const.DC_MSG_GIF, "gif": const.DC_MSG_GIF,
'audio': const.DC_MSG_AUDIO, "audio": const.DC_MSG_AUDIO,
'video': const.DC_MSG_VIDEO, "video": const.DC_MSG_VIDEO,
'file': const.DC_MSG_FILE, "file": const.DC_MSG_FILE,
'sticker': const.DC_MSG_STICKER, "sticker": const.DC_MSG_STICKER,
} }
@@ -424,14 +422,16 @@ def get_viewtype_code_from_name(view_type_name):
code = _view_type_mapping.get(view_type_name) code = _view_type_mapping.get(view_type_name)
if code is not None: if code is not None:
return code return code
raise ValueError("message typecode not found for {!r}, " raise ValueError(
"available {!r}".format(view_type_name, list(_view_type_mapping.keys()))) "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 # some helper code for turning system messages into hook events
# #
def map_system_message(msg): def map_system_message(msg):
if msg.is_system_message(): if msg.is_system_message():
res = parse_system_add_remove(msg.text) res = parse_system_add_remove(msg.text)
@@ -448,7 +448,7 @@ def map_system_message(msg):
def extract_addr(text): def extract_addr(text):
m = re.match(r'.*\((.+@.+)\)', text) m = re.match(r".*\((.+@.+)\)", text)
if m: if m:
text = m.group(1) text = m.group(1)
text = text.rstrip(".") text = text.rstrip(".")
@@ -467,7 +467,7 @@ def parse_system_add_remove(text):
# Group left by some one (tmp1@x.org). # Group left by some one (tmp1@x.org).
# Group left by tmp1@x.org. # Group left by tmp1@x.org.
text = text.lower() text = text.lower()
m = re.match(r'member (.+) (removed|added) by (.+)', text) m = re.match(r"member (.+) (removed|added) by (.+)", text)
if m: if m:
affected, action, actor = m.groups() affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor) return action, extract_addr(affected), extract_addr(actor)

View File

@@ -9,6 +9,7 @@ def with_doc(f):
# https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py # https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py
def cached(f): def cached(f):
"""returns a cached property that is calculated by function f""" """returns a cached property that is calculated by function f"""
def get(self): def get(self):
try: try:
return self._property_cache[f] return self._property_cache[f]

View File

@@ -26,14 +26,12 @@ class Provider(object):
@property @property
def overview_page(self) -> str: def overview_page(self) -> str:
"""URL to the overview page of the provider on providers.delta.chat.""" """URL to the overview page of the provider on providers.delta.chat."""
return from_dc_charpointer( return from_dc_charpointer(lib.dc_provider_get_overview_page(self._provider))
lib.dc_provider_get_overview_page(self._provider))
@property @property
def get_before_login_hints(self) -> str: def get_before_login_hints(self) -> str:
"""Should be shown to the user on login.""" """Should be shown to the user on login."""
return from_dc_charpointer( return from_dc_charpointer(lib.dc_provider_get_before_login_hint(self._provider))
lib.dc_provider_get_before_login_hint(self._provider))
@property @property
def status(self) -> int: def status(self) -> int:

View File

@@ -1,56 +1,62 @@
from __future__ import print_function from __future__ import print_function
import os
import sys
import io
import subprocess
import queue
import threading
import fnmatch import fnmatch
import io
import os
import pathlib
import queue
import subprocess
import sys
import threading
import time import time
import weakref import weakref
from queue import Queue from queue import Queue
from typing import List, Callable from typing import Callable, List
import pytest import pytest
import requests import requests
import pathlib
from . import Account, const, account_hookimpl, get_core_info
from .events import FFIEventLogger, FFIEventTracker
from _pytest._code import Source from _pytest._code import Source
import deltachat import deltachat
from . import Account, account_hookimpl, const, get_core_info
from .events import FFIEventLogger, FFIEventTracker
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("deltachat testplugin options") group = parser.getgroup("deltachat testplugin options")
group.addoption( group.addoption(
"--liveconfig", action="store", default=None, "--liveconfig",
help="a file with >=2 lines where each line " action="store",
"contains NAME=VALUE config settings for one account" default=None,
help="a file with >=2 lines where each line " "contains NAME=VALUE config settings for one account",
) )
group.addoption( group.addoption(
"--ignored", action="store_true", "--ignored",
action="store_true",
help="Also run tests marked with the ignored marker", help="Also run tests marked with the ignored marker",
) )
group.addoption( group.addoption(
"--strict-tls", action="store_true", "--strict-tls",
action="store_true",
help="Never accept invalid TLS certificates for test accounts", help="Never accept invalid TLS certificates for test accounts",
) )
group.addoption( group.addoption(
"--extra-info", action="store_true", "--extra-info",
help="show more info on failures (imap server state, config)" action="store_true",
help="show more info on failures (imap server state, config)",
) )
group.addoption( group.addoption(
"--debug-setup", action="store_true", "--debug-setup",
help="show events during configure and start io phases of online accounts" action="store_true",
help="show events during configure and start io phases of online accounts",
) )
def pytest_configure(config): def pytest_configure(config):
cfg = config.getoption('--liveconfig') cfg = config.getoption("--liveconfig")
if not cfg: if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL') cfg = os.getenv("DCC_NEW_TMP_EMAIL")
if cfg: if cfg:
config.option.liveconfig = cfg config.option.liveconfig = cfg
@@ -113,19 +119,21 @@ def pytest_configure(config):
def pytest_report_header(config, startdir): def pytest_report_header(config, startdir):
info = get_core_info() info = get_core_info()
summary = ['Deltachat core={} sqlite={} journal_mode={}'.format( summary = [
info['deltachat_core_version'], "Deltachat core={} sqlite={} journal_mode={}".format(
info['sqlite_version'], info["deltachat_core_version"],
info['journal_mode'], info["sqlite_version"],
)] info["journal_mode"],
)
]
cfg = config.option.liveconfig cfg = config.option.liveconfig
if cfg: if cfg:
if "?" in cfg: if "?" in cfg:
url, token = cfg.split("?", 1) url, token = cfg.split("?", 1)
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url)) summary.append("Liveconfig provider: {}?<token ommitted>".format(url))
else: else:
summary.append('Liveconfig file: {}'.format(cfg)) summary.append("Liveconfig file: {}".format(cfg))
return summary return summary
@@ -135,8 +143,8 @@ def testprocess(request):
class TestProcess: 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): def __init__(self, pytestconfig):
self.pytestconfig = pytestconfig self.pytestconfig = pytestconfig
self._addr2files = {} self._addr2files = {}
@@ -154,7 +162,7 @@ class TestProcess:
if not liveconfig_opt.startswith("http"): if not liveconfig_opt.startswith("http"):
for line in open(liveconfig_opt): for line in open(liveconfig_opt):
if line.strip() and not line.strip().startswith('#'): if line.strip() and not line.strip().startswith("#"):
d = {} d = {}
for part in line.split(): for part in line.split():
name, value = part.split("=") name, value = part.split("=")
@@ -170,8 +178,7 @@ class TestProcess:
except IndexError: except IndexError:
res = requests.post(liveconfig_opt) res = requests.post(liveconfig_opt)
if res.status_code != 200: if res.status_code != 200:
pytest.fail("newtmpuser count={} code={}: '{}'".format( pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text))
index, res.status_code, res.text))
d = res.json() d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"]) config = dict(addr=d["email"], mail_pw=d["password"])
print("newtmpuser {}: addr={}".format(index, config["addr"])) print("newtmpuser {}: addr={}".format(index, config["addr"]))
@@ -230,10 +237,13 @@ def data(request):
# because we are run from a dev-setup with pytest direct, # because we are run from a dev-setup with pytest direct,
# through tox, and then maybe also from deltachat-binding # through tox, and then maybe also from deltachat-binding
# users like "deltabot". # users like "deltabot".
self.paths = [os.path.normpath(x) for x in [ 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(request.fspath.strpath), "data"),
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data") os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data"),
]] ]
]
def get_path(self, bn): 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."""
@@ -257,6 +267,7 @@ class ACSetup:
and io & imap initialization phases. From tests, use the higher level and io & imap initialization phases. From tests, use the higher level
public ACFactory methods instead of its private helper class. public ACFactory methods instead of its private helper class.
""" """
CONFIGURING = "CONFIGURING" CONFIGURING = "CONFIGURING"
CONFIGURED = "CONFIGURED" CONFIGURED = "CONFIGURED"
IDLEREADY = "IDLEREADY" IDLEREADY = "IDLEREADY"
@@ -279,6 +290,7 @@ class ACSetup:
def start_configure(self, account, reconfigure=False): 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: class PendingTracker:
@account_hookimpl @account_hookimpl
def ac_configure_completed(this, success): def ac_configure_completed(this, success):
@@ -375,8 +387,7 @@ class ACFactory:
self._finalizers = [] self._finalizers = []
self._accounts = [] self._accounts = []
self._acsetup = ACSetup(testprocess, self.init_time) self._acsetup = ACSetup(testprocess, self.init_time)
self._preconfigured_keys = ["alice", "bob", "charlie", self._preconfigured_keys = ["alice", "bob", "charlie", "dom", "elena", "fiona"]
"dom", "elena", "fiona"]
self.set_logging_default(False) self.set_logging_default(False)
request.addfinalizer(self.finalize) request.addfinalizer(self.finalize)
@@ -461,11 +472,16 @@ class ACFactory:
ac = self.get_unconfigured_account() ac = self.get_unconfigured_account()
acname = ac._logid acname = ac._logid
addr = "{}@offline.org".format(acname) addr = "{}@offline.org".format(acname)
ac.update_config(dict( ac.update_config(
addr=addr, displayname=acname, mail_pw="123", dict(
configured_addr=addr, configured_mail_pw="123", addr=addr,
displayname=acname,
mail_pw="123",
configured_addr=addr,
configured_mail_pw="123",
configured="1", configured="1",
)) )
)
self._preconfigure_key(ac, addr) self._preconfigure_key(ac, addr)
self._acsetup.init_logging(ac) self._acsetup.init_logging(ac)
return ac return ac
@@ -531,8 +547,10 @@ class ACFactory:
sys.executable, sys.executable,
"-u", "-u",
fn, fn,
"--email", bot_cfg["addr"], "--email",
"--password", bot_cfg["mail_pw"], bot_cfg["addr"],
"--password",
bot_cfg["mail_pw"],
bot_ac.db_path, bot_ac.db_path,
] ]
if ffi: if ffi:
@@ -545,7 +563,7 @@ class ACFactory:
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
bufsize=0, # line buffering bufsize=0, # line buffering
close_fds=True, # close all FDs other than 0/1/2 close_fds=True, # close all FDs other than 0/1/2
universal_newlines=True # give back text universal_newlines=True, # give back text
) )
bot = BotProcess(popen, addr=bot_cfg["addr"]) bot = BotProcess(popen, addr=bot_cfg["addr"])
self._finalizers.append(bot.kill) self._finalizers.append(bot.kill)

View File

@@ -1,8 +1,7 @@
from queue import Queue from queue import Queue
from threading import Event from threading import Event
from .hookspec import account_hookimpl, Global from .hookspec import Global, account_hookimpl
class ImexFailed(RuntimeError): class ImexFailed(RuntimeError):
@@ -24,8 +23,9 @@ class ImexTracker:
while True: while True:
ev = self._imex_events.get(timeout=progress_timeout) ev = self._imex_events.get(timeout=progress_timeout)
if isinstance(ev, int) and ev >= target_progress: if isinstance(ev, int) and ev >= target_progress:
assert ev <= progress_upper_limit, \ assert ev <= progress_upper_limit, (
str(ev) + " exceeded upper progress limit " + str(progress_upper_limit) str(ev) + " exceeded upper progress limit " + str(progress_upper_limit)
)
return ev return ev
if ev == 0: if ev == 0:
return None return None

View File

@@ -1,10 +1,8 @@
import os import os
import platform import platform
import subprocess import subprocess
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
assert len(sys.argv) == 2 assert len(sys.argv) == 2
workspacedir = sys.argv[1] workspacedir = sys.argv[1]
@@ -13,5 +11,13 @@ if __name__ == "__main__":
if relpath.startswith("deltachat"): if relpath.startswith("deltachat"):
p = os.path.join(workspacedir, relpath) p = os.path.join(workspacedir, relpath)
subprocess.check_call( subprocess.check_call(
["auditwheel", "repair", p, "-w", workspacedir, [
"--plat", "manylinux2014_" + arch]) "auditwheel",
"repair",
p,
"-w",
workspacedir,
"--plat",
"manylinux2014_" + arch,
]
)

View File

@@ -1,8 +1,6 @@
import os import os
import sys
import subprocess import subprocess
import sys
if __name__ == "__main__": if __name__ == "__main__":
assert len(sys.argv) == 2 assert len(sys.argv) == 2

View File

@@ -1,9 +1,9 @@
import time
import threading
import pytest
import os import os
from queue import Queue, Empty import threading
import time
from queue import Empty, Queue
import pytest
import deltachat import deltachat
@@ -63,9 +63,7 @@ def test_db_busy_error(acfactory, tmpdir):
elif report_type == ReportType.message_echo: elif report_type == ReportType.message_echo:
continue continue
else: else:
raise ValueError("{} unknown report type {}, args={}".format( raise ValueError("{} unknown report type {}, args={}".format(addr, report_type, report_args))
addr, report_type, report_args
))
alive_count -= 1 alive_count -= 1
replier.log("shutting down") replier.log("shutting down")
replier.account.shutdown() replier.account.shutdown()
@@ -88,10 +86,7 @@ class AutoReplier:
self.current_sent = 0 self.current_sent = 0
self.addr = self.account.get_self_contact().addr self.addr = self.account.get_self_contact().addr
self._thread = threading.Thread( 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.setDaemon(True)
self._thread.start() self._thread.start()

View File

@@ -1,4 +1,5 @@
import sys import sys
import pytest import pytest
@@ -277,7 +278,7 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp):
assert len(chats) == 4 # two newly created chats + self-chat + device-chat 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] group_chat = [c for c in chats if c.get_name() == "group name"][0]
assert group_chat.is_group() 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() assert not private_chat.is_group()
group_messages = group_chat.get_messages() group_messages = group_chat.get_messages()
@@ -378,7 +379,7 @@ def test_ephemeral_timer(acfactory, lp):
lp.sec("ac1: check that ephemeral timer is set for chat") lp.sec("ac1: check that ephemeral timer is set for chat")
assert chat1.get_ephemeral_timer() == 60 assert chat1.get_ephemeral_timer() == 60
chat1_summary = chat1.get_summary() 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") lp.sec("ac2: receive system message about ephemeral timer modification")
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED") ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")

View File

@@ -1,10 +1,11 @@
import os import os
import sys
import queue import queue
import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from imap_tools import AND, U
import pytest import pytest
from imap_tools import AND, U
from deltachat import const from deltachat import const
from deltachat.hookspec import account_hookimpl from deltachat.hookspec import account_hookimpl
from deltachat.message import Message from deltachat.message import Message
@@ -78,7 +79,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp):
assert len(export_files) == 2 assert len(export_files) == 2
for x in export_files: for x in export_files:
assert x.startswith(dir.strpath) 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() ac1._evtracker.consume_events()
lp.sec("exported keys (private and public)") lp.sec("exported keys (private and public)")
@@ -86,8 +87,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp):
lp.indent(dir.strpath + os.sep + name) lp.indent(dir.strpath + os.sep + name)
lp.sec("importing into existing account") lp.sec("importing into existing account")
ac2.import_self_keys(dir.strpath) ac2.import_self_keys(dir.strpath)
key_id2, = ac2._evtracker.get_info_regex_groups( (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*", check_error=False)
r".*stored.*KeyId\((.*)\).*", check_error=False)
assert key_id2 == key_id assert key_id2 == key_id
@@ -689,7 +689,7 @@ def test_gossip_encryption_preference(acfactory, lp):
msg = ac1._evtracker.wait_next_incoming_message() msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "first message" assert msg.text == "first message"
assert not msg.is_encrypted() 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 assert msg.chat.get_encryption_info() == res
lp.sec("ac2 learns that ac3 prefers encryption") lp.sec("ac2 learns that ac3 prefers encryption")
ac2.create_chat(ac3) ac2.create_chat(ac3)
@@ -701,7 +701,7 @@ def test_gossip_encryption_preference(acfactory, lp):
lp.sec("ac3 does not know that ac1 prefers encryption") lp.sec("ac3 does not know that ac1 prefers encryption")
ac1.create_chat(ac3) ac1.create_chat(ac3)
chat = ac3.create_chat(ac1) 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 assert chat.get_encryption_info() == res
msg = chat.send_text("not encrypted") msg = chat.send_text("not encrypted")
msg = ac1._evtracker.wait_next_incoming_message() msg = ac1._evtracker.wait_next_incoming_message()
@@ -766,7 +766,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
def test_no_draft_if_cant_send(acfactory): def test_no_draft_if_cant_send(acfactory):
"""Tests that no quote can be set if the user can't send to this chat""" """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() device_chat = ac1.get_device_chat()
msg = Message.new_empty(ac1, "text") msg = Message.new_empty(ac1, "text")
device_chat.set_draft(msg) device_chat.set_draft(msg)
@@ -795,7 +795,9 @@ def test_dont_show_emails(acfactory, lp):
acfactory.bring_accounts_online() acfactory.bring_accounts_online()
ac1.stop_io() ac1.stop_io()
ac1.direct_imap.append("Drafts", """ ac1.direct_imap.append(
"Drafts",
"""
From: ac1 <{}> From: ac1 <{}>
Subject: subj Subject: subj
To: alice@example.org To: alice@example.org
@@ -803,8 +805,13 @@ def test_dont_show_emails(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
message in Drafts that is moved to Sent later message in Drafts that is moved to Sent later
""".format(ac1.get_config("configured_addr"))) """.format(
ac1.direct_imap.append("Sent", """ ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Sent",
"""
From: ac1 <{}> From: ac1 <{}>
Subject: subj Subject: subj
To: alice@example.org To: alice@example.org
@@ -812,8 +819,13 @@ def test_dont_show_emails(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
message in Sent message in Sent
""".format(ac1.get_config("configured_addr"))) """.format(
ac1.direct_imap.append("Spam", """ ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org From: unknown.address@junk.org
Subject: subj Subject: subj
To: {} To: {}
@@ -821,8 +833,13 @@ def test_dont_show_emails(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
Unknown message in Spam Unknown message in Spam
""".format(ac1.get_config("configured_addr"))) """.format(
ac1.direct_imap.append("Junk", """ ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Junk",
"""
From: unknown.address@junk.org From: unknown.address@junk.org
Subject: subj Subject: subj
To: {} To: {}
@@ -830,7 +847,10 @@ def test_dont_show_emails(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
Unknown message in Junk 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") ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message") lp.sec("All prepared, now let DC find the message")
@@ -1154,7 +1174,7 @@ def test_send_and_receive_image(acfactory, lp, data):
def test_import_export_online_all(acfactory, tmpdir, data, lp): 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") lp.sec("create some chat content")
chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat() chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat()
@@ -1387,8 +1407,7 @@ def test_add_remove_member_remote_events(acfactory, lp):
ev = in_list.get() ev = in_list.get()
assert ev.action == "chat-modified" assert ev.action == "chat-modified"
assert chat.is_promoted() assert chat.is_promoted()
assert sorted(x.addr for x in chat.get_contacts()) == \ assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts())
sorted(x.addr for x in ev.chat.get_contacts())
lp.sec("ac1: add address2") lp.sec("ac1: add address2")
# note that if the above create_chat() would not # note that if the above create_chat() would not
@@ -1530,8 +1549,10 @@ def test_connectivity(acfactory, lp):
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING) 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()`, " + lp.sec(
"all messages are fetched") "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") ac1.direct_imap.select_config_folder("inbox")
with ac1.direct_imap.idle() as idle1: with ac1.direct_imap.idle() as idle1:
@@ -1594,10 +1615,12 @@ def test_fetch_deleted_msg(acfactory, lp):
See https://github.com/deltachat/deltachat-core-rust/issues/2429. 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.stop_io()
ac1.direct_imap.append("INBOX", """ ac1.direct_imap.append(
"INBOX",
"""
From: alice <alice@example.org> From: alice <alice@example.org>
Subject: subj Subject: subj
To: bob@example.com To: bob@example.com
@@ -1606,7 +1629,8 @@ def test_fetch_deleted_msg(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
Deleted message Deleted message
""") """,
)
ac1.direct_imap.delete("1:*", expunge=False) ac1.direct_imap.delete("1:*", expunge=False)
ac1.start_io() ac1.start_io()
@@ -1888,11 +1912,26 @@ def test_group_quote(acfactory, lp):
assert received_reply.quote.id == out_msg.id assert received_reply.quote.id == out_msg.id
@pytest.mark.parametrize("folder,move,expected_destination,", [ @pytest.mark.parametrize(
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved "folder,move,expected_destination,",
("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 (
]) "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 # 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. # 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): def test_scan_folders(acfactory, lp, folder, move, expected_destination):

View File

@@ -2,9 +2,9 @@ from __future__ import print_function
import os.path import os.path
import shutil import shutil
from filecmp import cmp
import pytest import pytest
from filecmp import cmp
def wait_msg_delivered(account, msg_list): def wait_msg_delivered(account, msg_list):
@@ -38,7 +38,7 @@ class TestOnlineInCreation:
lp.sec("Creating in-creation file outside of blobdir") lp.sec("Creating in-creation file outside of blobdir")
assert tmpdir.strpath != ac1.get_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): with pytest.raises(Exception):
chat.prepare_message_file(src.strpath) chat.prepare_message_file(src.strpath)
@@ -48,11 +48,11 @@ class TestOnlineInCreation:
lp.sec("Creating file outside of blobdir") lp.sec("Creating file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir() assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt') src = tmpdir.join("file.txt")
src.write("hello there\n") src.write("hello there\n")
chat.send_file(src.strpath) 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" assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp): def test_forward_increation(self, acfactory, data, lp):
@@ -63,7 +63,7 @@ class TestOnlineInCreation:
lp.sec("create a message with a file in creation") lp.sec("create a message with a file in creation")
orig = data.get_path("d.png") 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: with open(path, "x") as fp:
fp.write("preparing") fp.write("preparing")
prepared_original = chat.prepare_message_file(path) prepared_original = chat.prepare_message_file(path)
@@ -94,10 +94,7 @@ class TestOnlineInCreation:
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered() assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
lp.sec("wait for both messages to be delivered to SMTP") lp.sec("wait for both messages to be delivered to SMTP")
wait_msg_delivered(ac1, [ wait_msg_delivered(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
(chat2.id, forwarded_id),
(chat.id, prepared_original.id)
])
lp.sec("wait1 for original or forwarded messages to arrive") lp.sec("wait1 for original or forwarded messages to arrive")
received_original = ac2._evtracker.wait_next_incoming_message() received_original = ac2._evtracker.wait_next_incoming_message()

View File

@@ -1,32 +1,50 @@
from __future__ import print_function from __future__ import print_function
import pytest
import os import os
import time 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 from datetime import datetime, timedelta, timezone
import pytest
@pytest.mark.parametrize("msgtext,res", [ from deltachat import Account, const
("Member Me (tmp1@x.org) removed by tmp2@x.org.", from deltachat.capi import ffi, lib
("removed", "tmp1@x.org", "tmp2@x.org")), from deltachat.cutil import iter_array
("Member With space (tmp1@x.org) removed by tmp2@x.org.", from deltachat.hookspec import account_hookimpl
("removed", "tmp1@x.org", "tmp2@x.org")), from deltachat.message import Message
("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", @pytest.mark.parametrize(
("removed", "tmp1@x.org", "me")), "msgtext,res",
("Group left by some one (tmp1@x.org).", [
("removed", "tmp1@x.org", "tmp1@x.org")), (
("Group left by tmp1@x.org.", "Member Me (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp1@x.org")), ("removed", "tmp1@x.org", "tmp2@x.org"),
("Member tmp1@x.org added by tmp2@x.org.", ("added", "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), ("Member nothing bla bla", None),
("Another unknown system message", None), ("Another unknown system message", None),
]) ],
)
def test_parse_system_add_remove(msgtext, res): def test_parse_system_add_remove(msgtext, res):
from deltachat.message import parse_system_add_remove from deltachat.message import parse_system_add_remove
@@ -424,11 +442,14 @@ class TestOfflineChat:
assert os.path.exists(msg.filename) assert os.path.exists(msg.filename)
assert msg.filemime == "image/png" assert msg.filemime == "image/png"
@pytest.mark.parametrize("typein,typeout", [ @pytest.mark.parametrize(
"typein,typeout",
[
(None, "application/octet-stream"), (None, "application/octet-stream"),
("text/plain", "text/plain"), ("text/plain", "text/plain"),
("image/png", "image/png"), ("image/png", "image/png"),
]) ],
)
def test_message_file(self, ac1, chat1, data, lp, typein, typeout): def test_message_file(self, ac1, chat1, data, lp, typein, typeout):
lp.sec("sending file") lp.sec("sending file")
fn = data.get_path("r.txt") fn = data.get_path("r.txt")
@@ -629,6 +650,6 @@ class TestOfflineChat:
lp.sec("check message count of only system messages (without daymarkers)") lp.sec("check message count of only system messages (without daymarkers)")
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0), 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 assert len(list(iter_array(dc_array, lambda x: x))) == 2

View File

@@ -1,24 +1,25 @@
import os import os
from queue import Queue 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.hookspec import global_hookimpl
from deltachat.capi import ffi from deltachat.testplugin import (
from deltachat.capi import lib ACSetup,
from deltachat.testplugin import ACSetup, create_dict_from_files_in_path, write_dict_to_dir create_dict_from_files_in_path,
write_dict_to_dir,
)
# from deltachat.account import EventLogger # from deltachat.account import EventLogger
class TestACSetup: class TestACSetup:
def test_cache_writing(self, tmp_path): def test_cache_writing(self, tmp_path):
base = tmp_path.joinpath("hello") base = tmp_path.joinpath("hello")
base.mkdir() base.mkdir()
d1 = base.joinpath("dir1") d1 = base.joinpath("dir1")
d1.mkdir() d1.mkdir()
d1.joinpath("file1").write_bytes(b'content1') d1.joinpath("file1").write_bytes(b"content1")
d2 = d1.joinpath("dir2") d2 = d1.joinpath("dir2")
d2.mkdir() d2.mkdir()
d2.joinpath("file2").write_bytes(b"123") d2.joinpath("file2").write_bytes(b"123")
@@ -103,6 +104,7 @@ def test_dc_close_events(tmpdir, acfactory):
def dc_account_after_shutdown(self, account): def dc_account_after_shutdown(self, account):
assert account._dc_context is None assert account._dc_context is None
shutdowns.put(account) shutdowns.put(account)
register_global_plugin(ShutdownPlugin()) register_global_plugin(ShutdownPlugin())
assert hasattr(ac1, "_dc_context") assert hasattr(ac1, "_dc_context")
ac1.shutdown() ac1.shutdown()
@@ -178,8 +180,8 @@ def test_get_info_open(tmpdir):
lib.dc_context_unref, lib.dc_context_unref,
) )
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx)) info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info assert "deltachat_core_version" in info
assert 'database_dir' in info assert "database_dir" in info
def test_logged_hook_failure(acfactory): def test_logged_hook_failure(acfactory):

View File

@@ -36,10 +36,14 @@ skipsdist = True
skip_install = True skip_install = True
deps = deps =
flake8 flake8
isort
black
# pygments required by rst-lint # pygments required by rst-lint
pygments pygments
restructuredtext_lint restructuredtext_lint
commands = commands =
isort --check --profile black src/deltachat examples/ tests/
black --check src/deltachat examples/ tests/
flake8 src/deltachat flake8 src/deltachat
flake8 tests/ examples/ flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst rst-lint --encoding 'utf-8' README.rst
@@ -85,3 +89,4 @@ markers =
[flake8] [flake8]
max-line-length = 120 max-line-length = 120
ignore = E203, E266, E501, W503