diff --git a/python/src/deltachat/__init__.py b/python/src/deltachat/__init__.py index b4593bb8..718eb42b 100644 --- a/python/src/deltachat/__init__.py +++ b/python/src/deltachat/__init__.py @@ -2,7 +2,7 @@ from deltachat import capi from deltachat.capi import ffi from deltachat.account import Account # noqa -__version__ = "0.5.dev1" +__version__ = "0.5.dev2" _DC_CALLBACK_MAP = {} diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index 89fc6795..44fccad9 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -1,4 +1,4 @@ -""" Delta.Chat Account class. """ +""" Account class implementation. """ from __future__ import print_function import threading @@ -9,13 +9,13 @@ try: from queue import Queue except ImportError: from Queue import Queue - -import deltachat -from . import capi -from .cutil import convert_to_bytes_utf8, ffi_unicode, iter_array_and_unref -from .capi import ffi, lib import attr from attr import validators as v + +import deltachat +from .capi import ffi, lib +from .cutil import convert_to_bytes_utf8, ffi_unicode, iter_array_and_unref +from .types import DC_Context from .chatting import Contact, Chat, Message @@ -23,6 +23,7 @@ class Account(object): """ An account contains configuration and provides methods for configuration, contact and chat creation and manipulation. """ + def __init__(self, db_path, logid=None): """ initialize account object. @@ -32,19 +33,16 @@ class Account(object): the default internal logging. """ - self.dc_context = ctx = capi.lib.dc_context_new( - capi.lib.py_dc_callback, - capi.ffi.NULL, capi.ffi.NULL) + self._dc_context = DC_Context( + lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL) + ) if hasattr(db_path, "encode"): db_path = db_path.encode("utf8") - capi.lib.dc_open(ctx, db_path, capi.ffi.NULL) - self._evhandler = EventHandler(self.dc_context) - self._evlogger = EventLogger(self.dc_context, logid) - self._threads = IOThreads(self.dc_context) - - def __del__(self): - if lib is not None: - lib.dc_context_unref(self.dc_context) + if not lib.dc_open(self._dc_context.p, db_path, ffi.NULL): + raise ValueError("Could not dc_open: {}".format(db_path)) + self._evhandler = EventHandler(self._dc_context) + self._evlogger = EventLogger(self._dc_context, logid) + self._threads = IOThreads(self._dc_context) def set_config(self, **kwargs): """ set configuration values. @@ -56,7 +54,7 @@ class Account(object): for name, value in kwargs.items(): name = name.encode("utf8") value = value.encode("utf8") - capi.lib.dc_set_config(self.dc_context, name, value) + lib.dc_set_config(self._dc_context.p, name, value) def get_config(self, name): """ return unicode string value. @@ -65,7 +63,7 @@ class Account(object): :returns: unicode value """ name = name.encode("utf8") - res = capi.lib.dc_get_config(self.dc_context, name, b'') + res = lib.dc_get_config(self._dc_context.p, name, b'') return ffi_unicode(res) def is_configured(self): @@ -73,7 +71,7 @@ class Account(object): :returns: True if account is configured. """ - return capi.lib.dc_is_configured(self.dc_context) + return lib.dc_is_configured(self._dc_context.p) def check_is_configured(self): """ Raise ValueError if this account is not configured. """ @@ -86,7 +84,7 @@ class Account(object): :returns: :class:`Contact` """ self.check_is_configured() - return Contact(self.dc_context, capi.lib.DC_CONTACT_ID_SELF) + return Contact(self._dc_context, lib.DC_CONTACT_ID_SELF) def create_contact(self, email, name=ffi.NULL): """ create a (new) Contact. If there already is a Contact @@ -99,8 +97,8 @@ class Account(object): """ name = convert_to_bytes_utf8(name) email = convert_to_bytes_utf8(email) - contact_id = capi.lib.dc_create_contact(self.dc_context, name, email) - return Contact(self.dc_context, contact_id) + contact_id = lib.dc_create_contact(self._dc_context.p, name, email) + return Contact(self._dc_context, contact_id) def get_contacts(self, query=ffi.NULL, with_self=False, only_verified=False): """ get a (filtered) list of contacts. @@ -117,8 +115,8 @@ class Account(object): flags |= lib.DC_GCL_VERIFIED_ONLY if with_self: flags |= lib.DC_GCL_ADD_SELF - dc_array_t = lib.dc_get_contacts(self.dc_context, flags, query) - return list(iter_array_and_unref(dc_array_t, lambda x: Contact(self.dc_context, x))) + dc_array_t = lib.dc_get_contacts(self._dc_context.p, flags, query) + return list(iter_array_and_unref(dc_array_t, lambda x: Contact(self._dc_context.p, x))) def create_chat_by_contact(self, contact): """ create or get an existing 1:1 chat object for the specified contact. @@ -128,9 +126,9 @@ class Account(object): """ contact_id = getattr(contact, "id", contact) assert isinstance(contact_id, int) - chat_id = capi.lib.dc_create_chat_by_contact_id( - self.dc_context, contact_id) - return Chat(self.dc_context, chat_id) + chat_id = lib.dc_create_chat_by_contact_id( + self._dc_context.p, contact_id) + return Chat(self._dc_context, chat_id) def create_chat_by_message(self, message): """ create or get an existing 1:1 chat object for the specified sender @@ -141,12 +139,12 @@ class Account(object): """ msg_id = getattr(message, "id", message) assert isinstance(msg_id, int) - chat_id = capi.lib.dc_create_chat_by_msg_id(self.dc_context, msg_id) - return Chat(self.dc_context, chat_id) + chat_id = lib.dc_create_chat_by_msg_id(self._dc_context.p, msg_id) + return Chat(self._dc_context, chat_id) def get_message_by_id(self, msg_id): """ return Message instance. """ - return Message(self.dc_context, msg_id) + return Message(self._dc_context, msg_id) def mark_seen_messages(self, messages): """ mark the given set of messages as seen. @@ -158,22 +156,22 @@ class Account(object): msg = getattr(msg, "id", msg) arr.append(msg) msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr)) - lib.dc_markseen_msgs(self.dc_context, msg_ids, len(messages)) + lib.dc_markseen_msgs(self._dc_context.p, msg_ids, len(messages)) def start(self): """ configure this account object, start receiving events, start IMAP/SMTP threads. """ - deltachat.set_context_callback(self.dc_context, self._process_event) - capi.lib.dc_configure(self.dc_context) + deltachat.set_context_callback(self._dc_context.p, self._process_event) + lib.dc_configure(self._dc_context.p) self._threads.start() def shutdown(self): """ shutdown IMAP/SMTP threads and stop receiving events""" - deltachat.clear_context_callback(self.dc_context) + deltachat.clear_context_callback(self._dc_context.p) self._threads.stop(wait=True) def _process_event(self, ctx, evt_name, data1, data2): - assert ctx == self.dc_context + assert ctx == self._dc_context.p self._evlogger(evt_name, data1, data2) method = getattr(self._evhandler, evt_name.lower(), None) if method is not None: @@ -183,7 +181,7 @@ class Account(object): class IOThreads: def __init__(self, dc_context): - self.dc_context = dc_context + self._dc_context = dc_context self._thread_quitflag = False self._name2thread = {} @@ -201,8 +199,8 @@ class IOThreads: def stop(self, wait=False): self._thread_quitflag = True - capi.lib.dc_interrupt_imap_idle(self.dc_context) - capi.lib.dc_interrupt_smtp_idle(self.dc_context) + lib.dc_interrupt_imap_idle(self._dc_context.p) + lib.dc_interrupt_smtp_idle(self._dc_context.p) if wait: for name, thread in self._name2thread.items(): thread.join() @@ -210,20 +208,20 @@ class IOThreads: def imap_thread_run(self): print ("starting imap thread") while not self._thread_quitflag: - capi.lib.dc_perform_imap_jobs(self.dc_context) - capi.lib.dc_perform_imap_fetch(self.dc_context) - capi.lib.dc_perform_imap_idle(self.dc_context) + lib.dc_perform_imap_jobs(self._dc_context.p) + lib.dc_perform_imap_fetch(self._dc_context.p) + lib.dc_perform_imap_idle(self._dc_context.p) def smtp_thread_run(self): print ("starting smtp thread") while not self._thread_quitflag: - capi.lib.dc_perform_smtp_jobs(self.dc_context) - capi.lib.dc_perform_smtp_idle(self.dc_context) + lib.dc_perform_smtp_jobs(self._dc_context.p) + lib.dc_perform_smtp_idle(self._dc_context.p) @attr.s class EventHandler(object): - dc_context = attr.ib(validator=v.instance_of(ffi.CData)) + _dc_context = attr.ib(validator=v.instance_of(DC_Context)) def read_url(self, url): try: @@ -239,7 +237,7 @@ class EventHandler(object): if not isinstance(content, bytes): content = content.encode("utf8") # we need to return a fresh pointer that the core owns - return capi.lib.dupstring_helper(content) + return lib.dupstring_helper(content) def dc_event_is_offline(self, data1, data2): return 0 # always online @@ -247,11 +245,11 @@ class EventHandler(object): class EventLogger: def __init__(self, dc_context, logid=None, debug=True): - self.dc_context = dc_context + self._dc_context = dc_context self._event_queue = Queue() self._debug = debug if logid is None: - logid = str(self.dc_context).strip(">").split()[-1] + logid = str(self._dc_context.p).strip(">").split()[-1] self.logid = logid self._timeout = None diff --git a/python/src/deltachat/chatting.py b/python/src/deltachat/chatting.py index 63360a6b..28751f95 100644 --- a/python/src/deltachat/chatting.py +++ b/python/src/deltachat/chatting.py @@ -1,9 +1,9 @@ """ chatting related objects: Contact, Chat, Message. """ -from . import capi from .cutil import convert_to_bytes_utf8, ffi_unicode, iter_array_and_unref -from .capi import ffi, lib +from .capi import lib from .types import cached_property, property_with_doc +from .types import DC_Context, DC_Contact, DC_Chat, DC_Msg import attr from attr import validators as v @@ -14,34 +14,30 @@ class Contact(object): You obtain instances of it through :class:`deltachat.account.Account`. """ - dc_context = attr.ib(validator=v.instance_of(ffi.CData)) + _dc_context = attr.ib(validator=v.instance_of(DC_Context)) id = attr.ib(validator=v.instance_of(int)) - @cached_property # only get it once because we only free it once - def dc_contact_t(self): - return capi.lib.dc_get_contact(self.dc_context, self.id) - - def __del__(self): - if lib is not None and hasattr(self, "_property_cache"): - lib.dc_contact_unref(self.dc_contact_t) + @cached_property + def _dc_contact(self): + return DC_Contact(lib.dc_get_contact(self._dc_context.p, self.id)) @property_with_doc def addr(self): """ normalized e-mail address for this account. """ - return ffi_unicode(capi.lib.dc_contact_get_addr(self.dc_contact_t)) + return ffi_unicode(lib.dc_contact_get_addr(self._dc_contact.p)) @property_with_doc def display_name(self): """ display name for this contact. """ - return ffi_unicode(capi.lib.dc_contact_get_display_name(self.dc_contact_t)) + return ffi_unicode(lib.dc_contact_get_display_name(self._dc_contact.p)) def is_blocked(self): """ Return True if the contact is blocked. """ - return capi.lib.dc_contact_is_blocked(self.dc_contact_t) + return lib.dc_contact_is_blocked(self._dc_contact.p) def is_verified(self): """ Return True if the contact is verified. """ - return capi.lib.dc_contact_is_verified(self.dc_contact_t) + return lib.dc_contact_is_verified(self._dc_contact.p) @attr.s @@ -50,17 +46,12 @@ class Chat(object): You obtain instances of it through :class:`deltachat.account.Account`. """ - - dc_context = attr.ib(validator=v.instance_of(ffi.CData)) + _dc_context = attr.ib(validator=v.instance_of(DC_Context)) id = attr.ib(validator=v.instance_of(int)) @cached_property - def dc_chat_t(self): - return capi.lib.dc_get_chat(self.dc_context, self.id) - - def __del__(self): - if lib is not None and hasattr(self, "_property_cache"): - lib.dc_chat_unref(self.dc_chat_t) + def _dc_chat(self): + return DC_Chat(lib.dc_get_chat(self._dc_context.p, self.id)) def is_deaddrop(self): """ return true if this chat is a deaddrop chat. """ @@ -73,31 +64,30 @@ class Chat(object): :returns: the resulting :class:`Message` instance """ msg = convert_to_bytes_utf8(msg) - print ("chat id", self.id) - msg_id = capi.lib.dc_send_text_msg(self.dc_context, self.id, msg) - return Message(self.dc_context, msg_id) + msg_id = lib.dc_send_text_msg(self._dc_context.p, self.id, msg) + return Message(self._dc_context, msg_id) def get_messages(self): """ return list of messages in this chat. :returns: list of :class:`Message` objects for this chat. """ - dc_array_t = lib.dc_get_chat_msgs(self.dc_context, self.id, 0, 0) - return list(iter_array_and_unref(dc_array_t, lambda x: Message(self.dc_context, x))) + dc_array_t = lib.dc_get_chat_msgs(self._dc_context.p, self.id, 0, 0) + return list(iter_array_and_unref(dc_array_t, lambda x: Message(self._dc_context, x))) def count_fresh_messages(self): """ return number of fresh messages in this chat. :returns: number of fresh messages """ - return lib.dc_get_fresh_msg_cnt(self.dc_context, self.id) + return lib.dc_get_fresh_msg_cnt(self._dc_context.p, self.id) def mark_noticed(self): """ mark all messages in this chat as noticed. Noticed messages are no longer fresh. """ - return lib.dc_marknoticed_chat(self.dc_context, self.id) + return lib.dc_marknoticed_chat(self._dc_context.p, self.id) @attr.s @@ -107,21 +97,18 @@ class Message(object): You obtain instances of it through :class:`deltachat.account.Account` or :class:`Chat`. """ - dc_context = attr.ib(validator=v.instance_of(ffi.CData)) + _dc_context = attr.ib(validator=v.instance_of(DC_Context)) id = attr.ib(validator=v.instance_of(int)) @cached_property - def dc_msg_t(self): - return capi.lib.dc_get_msg(self.dc_context, self.id) + def _dc_msg(self): + return DC_Msg(lib.dc_get_msg(self._dc_context.p, self.id)) def _refresh(self): - if hasattr(self, "_property_cache"): - lib.dc_msg_unref(self.dc_msg_t) - self._property_cache.clear() - - def __del__(self): - if lib is not None and hasattr(self, "_property_cache"): - lib.dc_msg_unref(self.dc_msg_t) + try: + del self._dc_msg + except KeyError: + pass def get_state(self): """ get the message in/out state. @@ -133,7 +120,7 @@ class Message(object): @property_with_doc def text(self): """unicode representation. """ - return ffi_unicode(capi.lib.dc_msg_get_text(self.dc_msg_t)) + return ffi_unicode(lib.dc_msg_get_text(self._dc_msg.p)) @property def chat(self): @@ -141,8 +128,8 @@ class Message(object): :returns: :class:`Chat` object """ - chat_id = capi.lib.dc_msg_get_chat_id(self.dc_msg_t) - return Chat(self.dc_context, chat_id) + chat_id = lib.dc_msg_get_chat_id(self._dc_msg.p) + return Chat(self._dc_context, chat_id) @attr.s @@ -154,7 +141,7 @@ class MessageState(object): @property def _msgstate(self): self.message._refresh() - return lib.dc_msg_get_state(self.message.dc_msg_t) + return lib.dc_msg_get_state(self.message._dc_msg.p) def is_in_fresh(self): """ return True if Message is incoming fresh message (un-noticed). diff --git a/python/src/deltachat/types.py b/python/src/deltachat/types.py index a2e7f072..769781b9 100644 --- a/python/src/deltachat/types.py +++ b/python/src/deltachat/types.py @@ -1,9 +1,36 @@ +from .capi import lib def property_with_doc(f): return property(f, None, None, f.__doc__) +class _UnrefStruct(object): + def __init__(self, c_obj): + self.p = c_obj + + def __del__(self): + obj = self.__dict__.pop("p", None) + if lib is not None and obj is not None: + self._unref(obj) + + +class DC_Context(_UnrefStruct): + _unref = lib.dc_context_unref + + +class DC_Contact(_UnrefStruct): + _unref = lib.dc_contact_unref + + +class DC_Chat(_UnrefStruct): + _unref = lib.dc_chat_unref + + +class DC_Msg(_UnrefStruct): + _unref = lib.dc_msg_unref + + # copied over unmodified from # https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py @@ -22,4 +49,9 @@ def cached_property(f): def set(self, val): propcache = self.__dict__.setdefault("_property_cache", {}) propcache[f] = val - return property(get, set) + + def fdel(self): + propcache = self.__dict__.setdefault("_property_cache", {}) + del propcache[f] + + return property(get, set, fdel) diff --git a/python/tests/test_account.py b/python/tests/test_account.py index d6575e50..87f4ac71 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -48,7 +48,6 @@ class TestOfflineAccount: assert chat2.id == chat.id assert chat == chat2 assert not (chat != chat2) - assert chat.dc_chat_t def test_message(self, acfactory): ac1 = acfactory.get_offline_account() @@ -77,14 +76,14 @@ class TestOnlineAccount: imap_ok = True if evt_name == "DC_EVENT_SMTP_CONNECTED": smtp_ok = True - print("** IMAP and SMTP logins successful", account.dc_context) + print("** IMAP and SMTP logins successful", account) def wait_configuration_progress(self, account, target): while 1: evt_name, data1, data2 = \ account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS") if data1 >= target: - print("** CONFIG PROGRESS {}".format(target), account.dc_context) + print("** CONFIG PROGRESS {}".format(target), account) break def test_selfcontact(self, acfactory): diff --git a/python/tests/test_lowlevel.py b/python/tests/test_lowlevel.py index 74332574..da26c6ff 100644 --- a/python/tests/test_lowlevel.py +++ b/python/tests/test_lowlevel.py @@ -1,5 +1,6 @@ from __future__ import print_function -from deltachat import capi +import pytest +from deltachat import capi, Account def test_empty_context(): @@ -7,6 +8,12 @@ def test_empty_context(): capi.lib.dc_close(ctx) +def test_wrong_db(tmpdir): + tmpdir.join("hello.db").write("123") + with pytest.raises(ValueError): + Account(db_path=tmpdir.strpath) + + def test_event_defines(): assert capi.lib.DC_EVENT_INFO == 100 assert capi.lib.DC_CONTACT_ID_SELF