Refine Python CI

Add lint environment to `deltachat-rpc-client/`
and set line length to 120, same as in `python/`.

Switch from flake8 to ruff.

Fix ruff warnings.
This commit is contained in:
link2xt
2023-01-19 00:31:39 +00:00
parent ef6f252842
commit fac7b064b4
41 changed files with 312 additions and 345 deletions

View File

@@ -144,7 +144,7 @@ jobs:
env: env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client working-directory: deltachat-rpc-client
run: tox -e py3 run: tox -e py3,lint
- name: install pypy - name: install pypy
if: ${{ matrix.python }} if: ${{ matrix.python }}

View File

@@ -27,9 +27,7 @@ async def log_error(event):
@hooks.on(events.MemberListChanged) @hooks.on(events.MemberListChanged)
async def on_memberlist_changed(event): async def on_memberlist_changed(event):
logging.info( logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
"member %s was %s", event.member, "added" if event.member_added else "removed"
)
@hooks.on(events.GroupImageChanged) @hooks.on(events.GroupImageChanged)

View File

@@ -27,3 +27,13 @@ deltachat_rpc_client = [
[project.entry-points.pytest11] [project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin" "deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
[tool.black]
line-length = 120
[tool.ruff]
select = ["E", "F", "W", "N", "YTT", "B", "C4", "ISC", "ICN", "PT", "RET", "SIM", "TID", "ARG", "DTZ", "ERA", "PLC", "PLE", "PLW", "PIE", "COM"]
line-length = 120
[tool.isort]
profile = "black"

View File

@@ -8,3 +8,18 @@ from .contact import Contact
from .deltachat import DeltaChat from .deltachat import DeltaChat
from .message import Message from .message import Message
from .rpc import Rpc from .rpc import Rpc
__all__ = [
"Account",
"AttrDict",
"Bot",
"Chat",
"Client",
"Contact",
"DeltaChat",
"EventType",
"Message",
"Rpc",
"run_bot_cli",
"run_client_cli",
]

View File

@@ -30,12 +30,7 @@ class AttrDict(dict):
"""Dictionary that allows accessing values usin the "dot notation" as attributes.""" """Dictionary that allows accessing values usin the "dot notation" as attributes."""
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__( super().__init__({_camel_to_snake(key): _to_attrdict(value) for key, value in dict(*args, **kwargs).items()})
{
_camel_to_snake(key): _to_attrdict(value)
for key, value in dict(*args, **kwargs).items()
}
)
def __getattr__(self, attr): def __getattr__(self, attr):
if attr in self: if attr in self:
@@ -51,7 +46,7 @@ class AttrDict(dict):
async def run_client_cli( async def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
**kwargs **kwargs,
) -> None: ) -> None:
"""Run a simple command line app, using the given hooks. """Run a simple command line app, using the given hooks.
@@ -65,7 +60,7 @@ async def run_client_cli(
async def run_bot_cli( async def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
**kwargs **kwargs,
) -> None: ) -> None:
"""Run a simple bot command line using the given hooks. """Run a simple bot command line using the given hooks.
@@ -80,7 +75,7 @@ async def _run_cli(
client_type: Type["Client"], client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
**kwargs **kwargs,
) -> None: ) -> None:
from .deltachat import DeltaChat from .deltachat import DeltaChat
from .rpc import Rpc from .rpc import Rpc
@@ -107,12 +102,9 @@ async def _run_cli(
client = client_type(account, hooks) client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version) client.logger.debug("Running deltachat core %s", core_version)
if not await client.is_configured(): if not await client.is_configured():
assert ( assert args.email, "Account is not configured and email must be provided"
args.email and args.password assert args.password, "Account is not configured and password must be provided"
), "Account is not configured and email and password must be provided" asyncio.create_task(client.configure(email=args.email, password=args.password))
asyncio.create_task(
client.configure(email=args.email, password=args.password)
)
await client.run_forever() await client.run_forever()

View File

@@ -89,9 +89,7 @@ class Account:
"""Configure an account.""" """Configure an account."""
await self._rpc.configure(self.id) await self._rpc.configure(self.id)
async def create_contact( async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
self, obj: Union[int, str, Contact], name: Optional[str] = None
) -> Contact:
"""Create a new Contact or return an existing one. """Create a new Contact or return an existing one.
Calling this method will always result in the same Calling this method will always result in the same
@@ -120,10 +118,7 @@ class Account:
async def get_blocked_contacts(self) -> List[AttrDict]: async def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts.""" """Return a list with snapshots of all blocked contacts."""
contacts = await self._rpc.get_blocked_contacts(self.id) contacts = await self._rpc.get_blocked_contacts(self.id)
return [ return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
AttrDict(contact=Contact(self, contact["id"]), **contact)
for contact in contacts
]
async def get_contacts( async def get_contacts(
self, self,
@@ -148,10 +143,7 @@ class Account:
if snapshot: if snapshot:
contacts = await self._rpc.get_contacts(self.id, flags, query) contacts = await self._rpc.get_contacts(self.id, flags, query)
return [ return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
AttrDict(contact=Contact(self, contact["id"]), **contact)
for contact in contacts
]
contacts = await self._rpc.get_contact_ids(self.id, flags, query) contacts = await self._rpc.get_contact_ids(self.id, flags, query)
return [Contact(self, contact_id) for contact_id in contacts] return [Contact(self, contact_id) for contact_id in contacts]
@@ -192,9 +184,7 @@ class Account:
if alldone_hint: if alldone_hint:
flags |= ChatlistFlag.ADD_ALLDONE_HINT flags |= ChatlistFlag.ADD_ALLDONE_HINT
entries = await self._rpc.get_chatlist_entries( entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
self.id, flags, query, contact and contact.id
)
if not snapshot: if not snapshot:
return [Chat(self, entry[0]) for entry in entries] return [Chat(self, entry[0]) for entry in entries]

View File

@@ -63,7 +63,7 @@ class Chat:
""" """
if duration is not None: if duration is not None:
assert duration > 0, "Invalid duration" assert duration > 0, "Invalid duration"
dur: Union[str, dict] = dict(Until=duration) dur: Union[str, dict] = {"Until": duration}
else: else:
dur = "Forever" dur = "Forever"
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur) await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
@@ -74,27 +74,19 @@ class Chat:
async def pin(self) -> None: async def pin(self) -> None:
"""Pin this chat.""" """Pin this chat."""
await self._rpc.set_chat_visibility( await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
self.account.id, self.id, ChatVisibility.PINNED
)
async def unpin(self) -> None: async def unpin(self) -> None:
"""Unpin this chat.""" """Unpin this chat."""
await self._rpc.set_chat_visibility( await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
self.account.id, self.id, ChatVisibility.NORMAL
)
async def archive(self) -> None: async def archive(self) -> None:
"""Archive this chat.""" """Archive this chat."""
await self._rpc.set_chat_visibility( await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
self.account.id, self.id, ChatVisibility.ARCHIVED
)
async def unarchive(self) -> None: async def unarchive(self) -> None:
"""Unarchive this chat.""" """Unarchive this chat."""
await self._rpc.set_chat_visibility( await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
self.account.id, self.id, ChatVisibility.NORMAL
)
async def set_name(self, name: str) -> None: async def set_name(self, name: str) -> None:
"""Set name of this chat.""" """Set name of this chat."""
@@ -133,9 +125,7 @@ class Chat:
if isinstance(quoted_msg, Message): if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id quoted_msg = quoted_msg.id
msg_id, _ = await self._rpc.misc_send_msg( msg_id, _ = await self._rpc.misc_send_msg(self.account.id, self.id, text, file, location, quoted_msg)
self.account.id, self.id, text, file, location, quoted_msg
)
return Message(self.account, msg_id) return Message(self.account, msg_id)
async def send_text(self, text: str) -> Message: async def send_text(self, text: str) -> Message:
@@ -241,23 +231,17 @@ class Chat:
timestamp_to: Optional[datetime] = None, timestamp_to: Optional[datetime] = None,
) -> List[AttrDict]: ) -> List[AttrDict]:
"""Get list of location snapshots for the given contact in the given timespan.""" """Get list of location snapshots for the given contact in the given timespan."""
time_from = ( time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
)
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0 time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
contact_id = contact.id if contact else 0 contact_id = contact.id if contact else 0
result = await self._rpc.get_locations( result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
self.account.id, self.id, contact_id, time_from, time_to
)
locations = [] locations = []
contacts: Dict[int, Contact] = {} contacts: Dict[int, Contact] = {}
for loc in result: for loc in result:
loc = AttrDict(loc) loc = AttrDict(loc)
loc["chat"] = self loc["chat"] = self
loc["contact"] = contacts.setdefault( loc["contact"] = contacts.setdefault(loc.contact_id, Contact(self.account, loc.contact_id))
loc.contact_id, Contact(self.account, loc.contact_id)
)
loc["message"] = Message(self.account, loc.msg_id) loc["message"] = Message(self.account, loc.msg_id)
locations.append(loc) locations.append(loc)
return locations return locations

View File

@@ -47,15 +47,11 @@ class Client:
self._should_process_messages = 0 self._should_process_messages = 0
self.add_hooks(hooks or []) self.add_hooks(hooks or [])
def add_hooks( def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]
) -> None:
for hook, event in hooks: for hook, event in hooks:
self.add_hook(hook, event) self.add_hook(hook, event)
def add_hook( def add_hook(self, hook: Callable, event: Union[type, EventFilter] = RawEvent) -> None:
self, hook: Callable, event: Union[type, EventFilter] = RawEvent
) -> None:
"""Register hook for the given event filter.""" """Register hook for the given event filter."""
if isinstance(event, type): if isinstance(event, type):
event = event() event = event()
@@ -64,7 +60,7 @@ class Client:
isinstance( isinstance(
event, event,
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged), (NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
) ),
) )
self._hooks.setdefault(type(event), set()).add((hook, event)) self._hooks.setdefault(type(event), set()).add((hook, event))
@@ -76,7 +72,7 @@ class Client:
isinstance( isinstance(
event, event,
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged), (NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
) ),
) )
self._hooks.get(type(event), set()).remove((hook, event)) self._hooks.get(type(event), set()).remove((hook, event))
@@ -95,9 +91,7 @@ class Client:
"""Process events forever.""" """Process events forever."""
await self.run_until(lambda _: False) await self.run_until(lambda _: False)
async def run_until( async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
self, func: Callable[[AttrDict], Union[bool, Coroutine]]
) -> AttrDict:
"""Process events until the given callable evaluates to True. """Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the The callable should accept an AttrDict object representing the
@@ -122,9 +116,7 @@ class Client:
if stop: if stop:
return event return event
async def _on_event( async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent
) -> None:
for hook, evfilter in self._hooks.get(filter_type, []): for hook, evfilter in self._hooks.get(filter_type, []):
if await evfilter.filter(event): if await evfilter.filter(event):
try: try:
@@ -133,11 +125,7 @@ class Client:
self.logger.exception(ex) self.logger.exception(ex)
async def _parse_command(self, event: AttrDict) -> None: async def _parse_command(self, event: AttrDict) -> None:
cmds = [ cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
hook[1].command
for hook in self._hooks.get(NewMessage, [])
if hook[1].command
]
parts = event.message_snapshot.text.split(maxsplit=1) parts = event.message_snapshot.text.split(maxsplit=1)
payload = parts[1] if len(parts) > 1 else "" payload = parts[1] if len(parts) > 1 else ""
cmd = parts.pop(0) cmd = parts.pop(0)
@@ -202,11 +190,7 @@ class Client:
for message in await self.account.get_fresh_messages_in_arrival_order(): for message in await self.account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot() snapshot = await message.get_snapshot()
await self._on_new_msg(snapshot) await self._on_new_msg(snapshot)
if ( if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
snapshot.is_info
and snapshot.system_message_type
!= SystemMessageType.WEBXDC_INFO_MESSAGE
):
await self._handle_info_msg(snapshot) await self._handle_info_msg(snapshot)
await snapshot.message.mark_seen() await snapshot.message.mark_seen()

View File

@@ -10,7 +10,7 @@ from .const import EventType
def _tuple_of(obj, type_: type) -> tuple: def _tuple_of(obj, type_: type) -> tuple:
if not obj: if not obj:
return tuple() return ()
if isinstance(obj, type_): if isinstance(obj, type_):
obj = (obj,) obj = (obj,)
@@ -39,7 +39,7 @@ class EventFilter(ABC):
"""Return True if two event filters are equal.""" """Return True if two event filters are equal."""
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self == other
async def _call_func(self, event) -> bool: async def _call_func(self, event) -> bool:
if not self.func: if not self.func:
@@ -65,9 +65,7 @@ class RawEvent(EventFilter):
should be dispatched or not. should be dispatched or not.
""" """
def __init__( def __init__(self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs):
self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs
):
super().__init__(**kwargs) super().__init__(**kwargs)
try: try:
self.types = _tuple_of(types, EventType) self.types = _tuple_of(types, EventType)

View File

@@ -49,22 +49,14 @@ class Message:
"""Mark the message as seen.""" """Mark the message as seen."""
await self._rpc.markseen_msgs(self.account.id, [self.id]) await self._rpc.markseen_msgs(self.account.id, [self.id])
async def send_webxdc_status_update( async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
self, update: Union[dict, str], description: str
) -> None:
"""Send a webxdc status update. This message must be a webxdc.""" """Send a webxdc status update. This message must be a webxdc."""
if not isinstance(update, str): if not isinstance(update, str):
update = json.dumps(update) update = json.dumps(update)
await self._rpc.send_webxdc_status_update( await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
self.account.id, self.id, update, description
)
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads( return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
await self._rpc.get_webxdc_status_updates(
self.account.id, self.id, last_known_serial
)
)
async def get_webxdc_info(self) -> dict: async def get_webxdc_info(self) -> dict:
return await self._rpc.get_webxdc_info(self.account.id, self.id) return await self._rpc.get_webxdc_info(self.account.id, self.id)

View File

@@ -67,9 +67,7 @@ class ACFactory:
) -> Message: ) -> Message:
if not from_account: if not from_account:
from_account = (await self.get_online_accounts(1))[0] from_account = (await self.get_online_accounts(1))[0]
to_contact = await from_account.create_contact( to_contact = await from_account.create_contact(await to_account.get_config("addr"))
await to_account.get_config("addr")
)
if group: if group:
to_chat = await from_account.create_group(group) to_chat = await from_account.create_group(group)
await to_chat.add_contact(to_contact) await to_chat.add_contact(to_contact)

View File

@@ -30,7 +30,7 @@ class Rpc:
"deltachat-rpc-server", "deltachat-rpc-server",
stdin=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
**self._kwargs **self._kwargs,
) )
self.id = 0 self.id = 0
self.event_queues = {} self.event_queues = {}
@@ -46,7 +46,7 @@ class Rpc:
await self.start() await self.start()
return self return self
async def __aexit__(self, exc_type, exc, tb): async def __aexit__(self, _exc_type, _exc, _tb):
await self.close() await self.close()
async def reader_loop(self) -> None: async def reader_loop(self) -> None:
@@ -97,5 +97,6 @@ class Rpc:
raise JsonRpcError(response["error"]) raise JsonRpcError(response["error"])
if "result" in response: if "result" in response:
return response["result"] return response["result"]
return None
return method return method

View File

@@ -6,14 +6,14 @@ from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError from deltachat_rpc_client.rpc import JsonRpcError
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_system_info(rpc) -> None: async def test_system_info(rpc) -> None:
system_info = await rpc.get_system_info() system_info = await rpc.get_system_info()
assert "arch" in system_info assert "arch" in system_info
assert "deltachat_core_version" in system_info assert "deltachat_core_version" in system_info
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_email_address_validity(rpc) -> None: async def test_email_address_validity(rpc) -> None:
valid_addresses = [ valid_addresses = [
"email@example.com", "email@example.com",
@@ -27,7 +27,7 @@ async def test_email_address_validity(rpc) -> None:
assert not await rpc.check_email_validity(addr) assert not await rpc.check_email_validity(addr)
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_acfactory(acfactory) -> None: async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account() account = await acfactory.new_configured_account()
while True: while True:
@@ -41,7 +41,7 @@ async def test_acfactory(acfactory) -> None:
print("Successful configuration") print("Successful configuration")
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_configure_starttls(acfactory) -> None: async def test_configure_starttls(acfactory) -> None:
account = await acfactory.new_preconfigured_account() account = await acfactory.new_preconfigured_account()
@@ -51,7 +51,7 @@ async def test_configure_starttls(acfactory) -> None:
assert await account.is_configured() assert await account.is_configured()
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_account(acfactory) -> None: async def test_account(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2) alice, bob = await acfactory.get_online_accounts(2)
@@ -111,7 +111,7 @@ async def test_account(acfactory) -> None:
await alice.stop_io() await alice.stop_io()
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_chat(acfactory) -> None: async def test_chat(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2) alice, bob = await acfactory.get_online_accounts(2)
@@ -177,7 +177,7 @@ async def test_chat(acfactory) -> None:
await group.get_locations() await group.get_locations()
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_contact(acfactory) -> None: async def test_contact(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2) alice, bob = await acfactory.get_online_accounts(2)
@@ -195,7 +195,7 @@ async def test_contact(acfactory) -> None:
await alice_contact_bob.create_chat() await alice_contact_bob.create_chat()
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_message(acfactory) -> None: async def test_message(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2) alice, bob = await acfactory.get_online_accounts(2)
@@ -226,7 +226,7 @@ async def test_message(acfactory) -> None:
await message.send_reaction("😎") await message.send_reaction("😎")
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_bot(acfactory) -> None: async def test_bot(acfactory) -> None:
mock = MagicMock() mock = MagicMock()
user = (await acfactory.get_online_accounts(1))[0] user = (await acfactory.get_online_accounts(1))[0]
@@ -237,25 +237,20 @@ async def test_bot(acfactory) -> None:
hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG) hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook) bot.add_hook(*hook)
event = await acfactory.process_message( event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
from_account=user, to_client=bot, text="Hello!"
)
mock.hook.assert_called_once_with(event.msg_id) mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook) bot.remove_hook(*hook)
track = lambda e: mock.hook(e.message_snapshot.id) def track(e):
mock.hook(e.message_snapshot.id)
mock.hook.reset_mock() mock.hook.reset_mock()
hook = track, events.NewMessage(r"hello") hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook) bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help")) bot.add_hook(track, events.NewMessage(command="/help"))
event = await acfactory.process_message( event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
from_account=user, to_client=bot, text="hello"
)
mock.hook.assert_called_with(event.msg_id) mock.hook.assert_called_with(event.msg_id)
event = await acfactory.process_message( event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
from_account=user, to_client=bot, text="hello!"
)
mock.hook.assert_called_with(event.msg_id) mock.hook.assert_called_with(event.msg_id)
await acfactory.process_message(from_account=user, to_client=bot, text="hey!") await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2 assert len(mock.hook.mock_calls) == 2
@@ -263,7 +258,5 @@ async def test_bot(acfactory) -> None:
mock.hook.reset_mock() mock.hook.reset_mock()
await acfactory.process_message(from_account=user, to_client=bot, text="hello") await acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message( event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
from_account=user, to_client=bot, text="/help"
)
mock.hook.assert_called_once_with(event.msg_id) mock.hook.assert_called_once_with(event.msg_id)

View File

@@ -3,16 +3,14 @@ import pytest
from deltachat_rpc_client import EventType from deltachat_rpc_client import EventType
@pytest.mark.asyncio @pytest.mark.asyncio()
async def test_webxdc(acfactory) -> None: async def test_webxdc(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2) alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr") bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob") alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat() alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_message( await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
text="Let's play chess!", file="../test-data/webxdc/chess.xdc"
)
while True: while True:
event = await bob.wait_for_event() event = await bob.wait_for_event()

View File

@@ -2,6 +2,7 @@
isolated_build = true isolated_build = true
envlist = envlist =
py3 py3
lint
[testenv] [testenv]
commands = commands =
@@ -16,3 +17,13 @@ deps =
pytest-asyncio pytest-asyncio
aiohttp aiohttp
aiodns aiodns
[testenv:lint]
skipsdist = True
skip_install = True
deps =
ruff
black
commands =
black --check src/ examples/ tests/
ruff src/ examples/ tests/

View File

@@ -11,7 +11,8 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys, os import sys
import os
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the

View File

@@ -34,8 +34,10 @@ class GroupTrackingPlugin:
def ac_member_added(self, chat, contact, actor, message): def ac_member_added(self, chat, contact, actor, message):
print( print(
"ac_member_added {} to chat {} from {}".format( "ac_member_added {} to chat {} from {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr 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))
@@ -44,8 +46,10 @@ class GroupTrackingPlugin:
def ac_member_removed(self, chat, contact, actor, message): def ac_member_removed(self, chat, contact, actor, message):
print( print(
"ac_member_removed {} from chat {} by {}".format( "ac_member_removed {} from chat {} by {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr contact.addr,
) chat.id,
actor or message.get_sender_contact().addr,
),
) )

View File

@@ -13,8 +13,8 @@ def datadir():
datadir = path.join("test-data") datadir = path.join("test-data")
if datadir.isdir(): if datadir.isdir():
return datadir return datadir
else: pytest.skip("test-data directory not found")
pytest.skip("test-data directory not found") return None
def test_echo_quit_plugin(acfactory, lp): def test_echo_quit_plugin(acfactory, lp):
@@ -47,7 +47,7 @@ def test_group_tracking_plugin(acfactory, lp):
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))
@@ -61,7 +61,7 @@ def test_group_tracking_plugin(acfactory, lp):
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")))
@@ -76,8 +76,9 @@ def test_group_tracking_plugin(acfactory, lp):
""" """
*ac_member_added {}*from*{}* *ac_member_added {}*from*{}*
""".format( """.format(
contact3.addr, ac1.get_config("addr") contact3.addr,
) ac1.get_config("addr"),
),
) )
lp.sec("contact successfully added, now removing") lp.sec("contact successfully added, now removing")
@@ -86,6 +87,7 @@ def test_group_tracking_plugin(acfactory, lp):
""" """
*ac_member_removed {}*from*{}* *ac_member_removed {}*from*{}*
""".format( """.format(
contact3.addr, ac1.get_config("addr") contact3.addr,
) ac1.get_config("addr"),
),
) )

View File

@@ -44,5 +44,9 @@ git_describe_command = "git describe --dirty --tags --long --match py-*.*"
[tool.black] [tool.black]
line-length = 120 line-length = 120
[tool.ruff]
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM"]
line-length = 120
[tool.isort] [tool.isort]
profile = "black" profile = "black"

View File

@@ -36,7 +36,8 @@ register_global_plugin(events)
def run_cmdline(argv=None, account_plugins=None): 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:

View File

@@ -102,8 +102,8 @@ 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:
@@ -198,7 +198,7 @@ def ffibuilder():
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)

View File

@@ -1,4 +1,4 @@
""" Account class implementation. """ """Account class implementation."""
from __future__ import print_function from __future__ import print_function
@@ -39,7 +39,7 @@ def get_core_info():
ffi.gc( 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,
) ),
) )
@@ -172,10 +172,7 @@ class Account(object):
namebytes = name.encode("utf8") namebytes = name.encode("utf8")
if isinstance(value, (int, bool)): if isinstance(value, (int, bool)):
value = str(int(value)) value = str(int(value))
if value is not None: valuebytes = value.encode("utf8") if value is not None else ffi.NULL
valuebytes = value.encode("utf8")
else:
valuebytes = ffi.NULL
lib.dc_set_config(self._dc_context, namebytes, valuebytes) lib.dc_set_config(self._dc_context, namebytes, valuebytes)
def get_config(self, name: str) -> str: def get_config(self, name: str) -> str:
@@ -225,9 +222,10 @@ class Account(object):
return bool(lib.dc_is_configured(self._dc_context)) return bool(lib.dc_is_configured(self._dc_context))
def is_open(self) -> bool: def is_open(self) -> bool:
"""Determine if account is open """Determine if account is open.
:returns True if account is open.""" :returns True if account is open.
"""
return bool(lib.dc_context_is_open(self._dc_context)) return bool(lib.dc_context_is_open(self._dc_context))
def set_avatar(self, img_path: Optional[str]) -> None: def set_avatar(self, img_path: Optional[str]) -> None:
@@ -543,7 +541,7 @@ class Account(object):
return from_dc_charpointer(res) return from_dc_charpointer(res)
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(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref) res = ffi.gc(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:
@@ -662,7 +660,7 @@ class Account(object):
return lib.dc_all_work_done(self._dc_context) return lib.dc_all_work_done(self._dc_context)
def start_io(self): def start_io(self):
"""start this account's IO scheduling (Rust-core async scheduler) """start this account's IO scheduling (Rust-core async scheduler).
If this account is not configured an Exception is raised. If this account is not configured an Exception is raised.
You need to call account.configure() and account.wait_configure_finish() You need to call account.configure() and account.wait_configure_finish()
@@ -705,12 +703,10 @@ class Account(object):
""" """
lib.dc_maybe_network(self._dc_context) lib.dc_maybe_network(self._dc_context)
def configure(self, reconfigure: bool = False) -> ConfigureTracker: def configure(self) -> ConfigureTracker:
"""Start configuration process and return a Configtracker instance """Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success on which you can block with wait_finish() to get a True/False success
value for the configuration process. value for the configuration process.
:param reconfigure: deprecated, doesn't need to be checked anymore.
""" """
if not self.get_config("addr") or not self.get_config("mail_pw"): if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config") raise MissingCredentials("addr or mail_pwd not set in config")
@@ -733,7 +729,8 @@ class Account(object):
def shutdown(self) -> None: def shutdown(self) -> None:
"""shutdown and destroy account (stop callback thread, close and remove """shutdown and destroy account (stop callback thread, close and remove
underlying dc_context).""" underlying dc_context).
"""
if self._dc_context is None: if self._dc_context is None:
return return

View File

@@ -1,4 +1,4 @@
""" Chat and Location related API. """ """Chat and Location related API."""
import calendar import calendar
import json import json
@@ -37,7 +37,7 @@ class Chat(object):
return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context
def __ne__(self, other) -> bool: def __ne__(self, other) -> bool:
return not (self == other) return not self == other
def __repr__(self) -> str: def __repr__(self) -> str:
return "<Chat id={} name={}>".format(self.id, self.get_name()) return "<Chat id={} name={}>".format(self.id, self.get_name())
@@ -74,19 +74,19 @@ class Chat(object):
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
def is_single(self) -> bool: def is_single(self) -> bool:
"""Return True if this chat is a single/direct chat, False otherwise""" """Return True if this chat is a single/direct chat, False otherwise."""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_SINGLE return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_SINGLE
def is_mailinglist(self) -> bool: def is_mailinglist(self) -> bool:
"""Return True if this chat is a mailing list, False otherwise""" """Return True if this chat is a mailing list, False otherwise."""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_MAILINGLIST return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_MAILINGLIST
def is_broadcast(self) -> bool: def is_broadcast(self) -> bool:
"""Return True if this chat is a broadcast list, False otherwise""" """Return True if this chat is a broadcast list, False otherwise."""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_BROADCAST return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_BROADCAST
def is_multiuser(self) -> bool: def is_multiuser(self) -> bool:
"""Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise""" """Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise."""
return lib.dc_chat_get_type(self._dc_chat) in ( return lib.dc_chat_get_type(self._dc_chat) in (
const.DC_CHAT_TYPE_GROUP, const.DC_CHAT_TYPE_GROUP,
const.DC_CHAT_TYPE_MAILINGLIST, const.DC_CHAT_TYPE_MAILINGLIST,
@@ -94,11 +94,11 @@ class Chat(object):
) )
def is_self_talk(self) -> bool: def is_self_talk(self) -> bool:
"""Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise""" """Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise."""
return bool(lib.dc_chat_is_self_talk(self._dc_chat)) return bool(lib.dc_chat_is_self_talk(self._dc_chat))
def is_device_talk(self) -> bool: def is_device_talk(self) -> bool:
"""Returns True if this chat is the "Device Messages" chat, False otherwise""" """Returns True if this chat is the "Device Messages" chat, False otherwise."""
return bool(lib.dc_chat_is_device_talk(self._dc_chat)) return bool(lib.dc_chat_is_device_talk(self._dc_chat))
def is_muted(self) -> bool: def is_muted(self) -> bool:
@@ -109,12 +109,12 @@ class Chat(object):
return bool(lib.dc_chat_is_muted(self._dc_chat)) return bool(lib.dc_chat_is_muted(self._dc_chat))
def is_pinned(self) -> bool: def is_pinned(self) -> bool:
"""Return True if this chat is pinned, False otherwise""" """Return True if this chat is pinned, False otherwise."""
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_PINNED return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_PINNED
def is_archived(self) -> bool: def is_archived(self) -> bool:
"""Return True if this chat is archived, False otherwise. """Return True if this chat is archived, False otherwise.
:returns: True if archived, False otherwise :returns: True if archived, False otherwise.
""" """
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
@@ -136,7 +136,7 @@ class Chat(object):
def can_send(self) -> bool: def can_send(self) -> bool:
"""Check if messages can be sent to a give chat. """Check if messages can be sent to a give chat.
This is not true eg. for the contact requests or for the device-talk This is not true eg. for the contact requests or for the device-talk.
:returns: True if the chat is writable, False otherwise :returns: True if the chat is writable, False otherwise
""" """
@@ -167,7 +167,7 @@ class Chat(object):
def get_color(self): def get_color(self):
"""return the color of the chat. """return the color of the chat.
:returns: color as 0x00rrggbb :returns: color as 0x00rrggbb.
""" """
return lib.dc_chat_get_color(self._dc_chat) return lib.dc_chat_get_color(self._dc_chat)
@@ -178,21 +178,18 @@ class Chat(object):
return json.loads(s) return json.loads(s)
def mute(self, duration: Optional[int] = None) -> None: def mute(self, duration: Optional[int] = None) -> None:
"""mutes the chat """mutes the chat.
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again. :param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None :returns: None
""" """
if duration is None: mute_duration = -1 if duration is None else duration
mute_duration = -1
else:
mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration) ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
if not bool(ret): if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed") raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self) -> None: def unmute(self) -> None:
"""unmutes the chat """unmutes the chat.
:returns: None :returns: None
""" """
@@ -252,7 +249,8 @@ class Chat(object):
def get_encryption_info(self) -> Optional[str]: def get_encryption_info(self) -> Optional[str]:
"""Return encryption info for this chat. """Return encryption info for this chat.
:returns: a string with encryption preferences of all chat members""" :returns: a string with encryption preferences of all chat members
"""
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id) res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
return from_dc_charpointer(res) return from_dc_charpointer(res)
@@ -463,7 +461,7 @@ class Chat(object):
def get_contacts(self): def get_contacts(self):
"""get all contacts for this chat. """get all contacts for this chat.
:returns: list of :class:`deltachat.contact.Contact` objects for this chat :returns: list of :class:`deltachat.contact.Contact` objects for this chat.
""" """
from .contact import Contact from .contact import Contact
@@ -547,19 +545,10 @@ class Chat(object):
:param timespan_to: a datetime object or None (indicating up till now) :param timespan_to: a datetime object or None (indicating up till now)
:returns: list of :class:`deltachat.chat.Location` objects. :returns: list of :class:`deltachat.chat.Location` objects.
""" """
if timestamp_from is None: time_from = 0 if timestamp_from is None else calendar.timegm(timestamp_from.utctimetuple())
time_from = 0 time_to = 0 if timestamp_to is None else calendar.timegm(timestamp_to.utctimetuple())
else:
time_from = calendar.timegm(timestamp_from.utctimetuple())
if timestamp_to is None:
time_to = 0
else:
time_to = calendar.timegm(timestamp_to.utctimetuple())
if contact is None: contact_id = 0 if contact is None else contact.id
contact_id = 0
else:
contact_id = contact.id
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to) dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
return [ return [

View File

@@ -1,4 +1,4 @@
""" Contact object. """ """Contact object."""
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
from typing import Optional from typing import Optional
@@ -28,7 +28,7 @@ class Contact(object):
return self.account._dc_context == other.account._dc_context and self.id == other.id return self.account._dc_context == other.account._dc_context and self.id == other.id
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not self == other
def __repr__(self): def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context) return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
@@ -76,7 +76,7 @@ class Contact(object):
return lib.dc_contact_is_verified(self._dc_contact) return lib.dc_contact_is_verified(self._dc_contact)
def get_verifier(self, contact): def get_verifier(self, contact):
"""Return the address of the contact that verified the contact""" """Return the address of the contact that verified the contact."""
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact)) return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
def get_profile_image(self) -> Optional[str]: def get_profile_image(self) -> Optional[str]:

View File

@@ -79,15 +79,17 @@ class DirectImap:
def select_config_folder(self, config_name: str): def select_config_folder(self, config_name: str):
"""Return info about selected folder if it is """Return info about selected folder if it is
configured, otherwise None.""" configured, otherwise None.
"""
if "_" not in config_name: if "_" not in config_name:
config_name = "configured_{}_folder".format(config_name) config_name = "configured_{}_folder".format(config_name)
foldername = self.account.get_config(config_name) foldername = self.account.get_config(config_name)
if foldername: if foldername:
return self.select_folder(foldername) return self.select_folder(foldername)
return None
def list_folders(self) -> List[str]: def list_folders(self) -> List[str]:
"""return list of all existing folder names""" """return list of all existing folder names."""
assert not self._idling assert not self._idling
return [folder.name for folder in self.conn.folder.list()] return [folder.name for folder in self.conn.folder.list()]
@@ -103,7 +105,7 @@ class DirectImap:
def get_all_messages(self) -> List[MailMessage]: def get_all_messages(self) -> List[MailMessage]:
assert not self._idling assert not self._idling
return [mail for mail in self.conn.fetch()] return list(self.conn.fetch())
def get_unread_messages(self) -> List[str]: def get_unread_messages(self) -> List[str]:
assert not self._idling assert not self._idling
@@ -221,5 +223,4 @@ class IdleManager:
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."""
res = self.direct_imap.conn.idle.stop() return self.direct_imap.conn.idle.stop()
return res

View File

@@ -32,12 +32,11 @@ class FFIEvent:
def __str__(self): def __str__(self):
if self.name == "DC_EVENT_INFO": if self.name == "DC_EVENT_INFO":
return "INFO {data2}".format(data2=self.data2) return "INFO {data2}".format(data2=self.data2)
elif self.name == "DC_EVENT_WARNING": if self.name == "DC_EVENT_WARNING":
return "WARNING {data2}".format(data2=self.data2) return "WARNING {data2}".format(data2=self.data2)
elif self.name == "DC_EVENT_ERROR": if self.name == "DC_EVENT_ERROR":
return "ERROR {data2}".format(data2=self.data2) return "ERROR {data2}".format(data2=self.data2)
else: return "{name} data1={data1} data2={data2}".format(**self.__dict__)
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
class FFIEventLogger: class FFIEventLogger:
@@ -135,7 +134,8 @@ class FFIEventTracker:
def wait_for_connectivity(self, connectivity): def wait_for_connectivity(self, connectivity):
"""Wait for the specified connectivity. """Wait for the specified connectivity.
This only works reliably if the connectivity doesn't change This only works reliably if the connectivity doesn't change
again too quickly, otherwise we might miss it.""" again too quickly, otherwise we might miss it.
"""
while 1: while 1:
if self.account.get_connectivity() == connectivity: if self.account.get_connectivity() == connectivity:
return return
@@ -143,12 +143,13 @@ class FFIEventTracker:
def wait_for_connectivity_change(self, previous, expected_next): def wait_for_connectivity_change(self, previous, expected_next):
"""Wait until the connectivity changes to `expected_next`. """Wait until the connectivity changes to `expected_next`.
Fails the test if it changes to something else.""" Fails the test if it changes to something else.
"""
while 1: while 1:
current = self.account.get_connectivity() current = self.account.get_connectivity()
if current == expected_next: if current == expected_next:
return return
elif current != previous: if current != previous:
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current)) raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED") self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -183,7 +184,8 @@ class FFIEventTracker:
- ac1 and ac2 are created - ac1 and ac2 are created
- ac1 sends a message to ac2 - ac1 sends a message to ac2
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message - ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
- therefore no DC_EVENT_INCOMING_MSG is sent""" - therefore no DC_EVENT_INCOMING_MSG is sent
"""
self.get_info_contains("INBOX: Idle entering") self.get_info_contains("INBOX: Idle entering")
def wait_next_incoming_message(self): def wait_next_incoming_message(self):
@@ -193,14 +195,15 @@ class FFIEventTracker:
def wait_next_messages_changed(self): def wait_next_messages_changed(self):
"""wait for and return next message-changed message or None """wait for and return next message-changed message or None
if the event contains no msgid""" if the event contains no msgid
"""
ev = self.get_matching("DC_EVENT_MSGS_CHANGED") ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0: if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2) return self.account.get_message_by_id(ev.data2)
return None return None
def wait_next_reactions_changed(self): def wait_next_reactions_changed(self):
"""wait for and return next reactions-changed message""" """wait for and return next reactions-changed message."""
ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED") ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED")
assert ev.data1 > 0 assert ev.data1 > 0
return self.account.get_message_by_id(ev.data2) return self.account.get_message_by_id(ev.data2)
@@ -292,10 +295,10 @@ class EventThread(threading.Thread):
if data1 == 0 or data1 == 1000: if data1 == 0 or data1 == 1000:
success = data1 == 1000 success = data1 == 1000
comment = ffi_event.data2 comment = ffi_event.data2
yield "ac_configure_completed", dict(success=success, comment=comment) yield "ac_configure_completed", {"success": success, "comment": comment}
elif name == "DC_EVENT_INCOMING_MSG": elif name == "DC_EVENT_INCOMING_MSG":
msg = account.get_message_by_id(ffi_event.data2) msg = account.get_message_by_id(ffi_event.data2)
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg)) yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
elif name == "DC_EVENT_MSGS_CHANGED": elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0: if ffi_event.data2 != 0:
msg = account.get_message_by_id(ffi_event.data2) msg = account.get_message_by_id(ffi_event.data2)
@@ -303,19 +306,19 @@ class EventThread(threading.Thread):
res = map_system_message(msg) res = map_system_message(msg)
if res and res[0].startswith("ac_member"): if res and res[0].startswith("ac_member"):
yield res yield res
yield "ac_outgoing_message", dict(message=msg) yield "ac_outgoing_message", {"message": msg}
elif msg.is_in_fresh(): elif msg.is_in_fresh():
yield map_system_message(msg) or ( yield map_system_message(msg) or (
"ac_incoming_message", "ac_incoming_message",
dict(message=msg), {"message": msg},
) )
elif name == "DC_EVENT_REACTIONS_CHANGED": elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0 assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2) msg = account.get_message_by_id(ffi_event.data2)
yield "ac_reactions_changed", dict(message=msg) yield "ac_reactions_changed", {"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", {"message": msg}
elif name == "DC_EVENT_CHAT_MODIFIED": elif name == "DC_EVENT_CHAT_MODIFIED":
chat = account.get_chat_by_id(ffi_event.data1) chat = account.get_chat_by_id(ffi_event.data1)
yield "ac_chat_modified", dict(chat=chat) yield "ac_chat_modified", {"chat": chat}

View File

@@ -1,4 +1,4 @@
""" Hooks for Python bindings to Delta Chat Core Rust CFFI""" """Hooks for Python bindings to Delta Chat Core Rust CFFI."""
import pluggy import pluggy

View File

@@ -1,4 +1,4 @@
""" The Message object. """ """The Message object."""
import json import json
import os import os
@@ -59,10 +59,7 @@ class Message(object):
:param view_type: the message type code or one of the strings: :param view_type: the message type code or one of the strings:
"text", "audio", "video", "file", "sticker", "videochat", "webxdc" "text", "audio", "video", "file", "sticker", "videochat", "webxdc"
""" """
if isinstance(view_type, int): view_type_code = view_type if isinstance(view_type, int) else get_viewtype_code_from_name(view_type)
view_type_code = view_type
else:
view_type_code = get_viewtype_code_from_name(view_type)
return Message( return Message(
account, account,
ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref), ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref),
@@ -129,7 +126,7 @@ class Message(object):
@props.with_doc @props.with_doc
def filemime(self) -> str: def filemime(self) -> str:
"""mime type of the file (if it exists)""" """mime type of the file (if it exists)."""
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg)) return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
def get_status_updates(self, serial: int = 0) -> list: def get_status_updates(self, serial: int = 0) -> list:
@@ -141,7 +138,7 @@ class Message(object):
:param serial: The last known serial. Pass 0 if there are no known serials to receive all updates. :param serial: The last known serial. Pass 0 if there are no known serials to receive all updates.
""" """
return json.loads( return json.loads(
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial)) from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial)),
) )
def send_status_update(self, json_data: Union[str, dict], description: str) -> bool: def send_status_update(self, json_data: Union[str, dict], description: str) -> bool:
@@ -158,8 +155,11 @@ class Message(object):
json_data = json.dumps(json_data, default=str) json_data = json.dumps(json_data, default=str)
return bool( return bool(
lib.dc_send_webxdc_status_update( lib.dc_send_webxdc_status_update(
self.account._dc_context, self.id, as_dc_charpointer(json_data), as_dc_charpointer(description) self.account._dc_context,
) self.id,
as_dc_charpointer(json_data),
as_dc_charpointer(description),
),
) )
def send_reaction(self, reaction: str): def send_reaction(self, reaction: str):
@@ -232,16 +232,18 @@ class Message(object):
ts = lib.dc_msg_get_received_timestamp(self._dc_msg) ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
if ts: if ts:
return datetime.fromtimestamp(ts, timezone.utc) return datetime.fromtimestamp(ts, timezone.utc)
return None
@props.with_doc @props.with_doc
def ephemeral_timer(self): def ephemeral_timer(self):
"""Ephemeral timer in seconds """Ephemeral timer in seconds.
:returns: timer in seconds or None if there is no timer :returns: timer in seconds or None if there is no timer
""" """
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg) timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
if timer: if timer:
return timer return timer
return None
@props.with_doc @props.with_doc
def ephemeral_timestamp(self): def ephemeral_timestamp(self):
@@ -255,23 +257,25 @@ class Message(object):
@property @property
def quoted_text(self) -> Optional[str]: def quoted_text(self) -> Optional[str]:
"""Text inside the quote """Text inside the quote.
:returns: Quoted text""" :returns: Quoted text
"""
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg)) return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
@property @property
def quote(self): def quote(self):
"""Quote getter """Quote getter.
:returns: Quoted message, if found in the database""" :returns: Quoted message, if found in the database
"""
msg = lib.dc_msg_get_quoted_msg(self._dc_msg) msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
if msg: if msg:
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref)) return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
@quote.setter @quote.setter
def quote(self, quoted_message): def quote(self, quoted_message):
"""Quote setter""" """Quote setter."""
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg) lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
def force_plaintext(self) -> None: def force_plaintext(self) -> None:
@@ -286,7 +290,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
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:
@@ -297,7 +301,7 @@ class Message(object):
@property @property
def error(self) -> Optional[str]: def error(self) -> Optional[str]:
"""Error message""" """Error message."""
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg)) return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
@property @property
@@ -493,7 +497,8 @@ def get_viewtype_code_from_name(view_type_name):
if code is not None: if code is not None:
return code return code
raise ValueError( raise ValueError(
"message typecode not found for {!r}, " "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())),
) )
@@ -506,14 +511,11 @@ 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)
if not res: if not res:
return return None
action, affected, actor = res action, affected, actor = res
affected = msg.account.get_contact_by_addr(affected) affected = msg.account.get_contact_by_addr(affected)
if actor == "me": actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
actor = None d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
else:
actor = msg.account.get_contact_by_addr(actor)
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
return "ac_member_" + res[0], d return "ac_member_" + res[0], d
@@ -528,8 +530,8 @@ def extract_addr(text):
def parse_system_add_remove(text): def parse_system_add_remove(text):
"""return add/remove info from parsing the given system message text. """return add/remove info from parsing the given system message text.
returns a (action, affected, actor) triple""" returns a (action, affected, actor) triple
"""
# You removed member a@b. # You removed member a@b.
# You added member a@b. # You added member a@b.
# Member Me (x@y) removed by a@b. # Member Me (x@y) removed by a@b.

View File

@@ -8,7 +8,7 @@ def with_doc(f):
# copied over unmodified from # copied over unmodified from
# 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:
@@ -17,8 +17,9 @@ def cached(f):
self._property_cache = {} self._property_cache = {}
except KeyError: except KeyError:
pass pass
x = self._property_cache[f] = f(self) res = f(self)
return x self._property_cache[f] = res
return res
def set(self, val): def set(self, val):
propcache = self.__dict__.setdefault("_property_cache", {}) propcache = self.__dict__.setdefault("_property_cache", {})

View File

@@ -9,7 +9,8 @@ class ProviderNotFoundError(Exception):
class Provider(object): class Provider(object):
"""Provider information. """
Provider information.
:param domain: The email to get the provider info for. :param domain: The email to get the provider info for.
""" """

View File

@@ -1,4 +1,4 @@
""" The Reactions object. """ """The Reactions object."""
from .capi import ffi, lib from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array from .cutil import from_dc_charpointer, iter_array

View File

@@ -29,7 +29,7 @@ def pytest_addoption(parser):
"--liveconfig", "--liveconfig",
action="store", action="store",
default=None, default=None,
help="a file with >=2 lines where each line " "contains NAME=VALUE config settings for one account", help="a file with >=2 lines where each line contains NAME=VALUE config settings for one account",
) )
group.addoption( group.addoption(
"--ignored", "--ignored",
@@ -124,7 +124,7 @@ def pytest_report_header(config, startdir):
info["deltachat_core_version"], info["deltachat_core_version"],
info["sqlite_version"], info["sqlite_version"],
info["journal_mode"], info["journal_mode"],
) ),
] ]
cfg = config.option.liveconfig cfg = config.option.liveconfig
@@ -180,7 +180,7 @@ class TestProcess:
if res.status_code != 200: if res.status_code != 200:
pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text)) pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text))
d = res.json() d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"]) config = {"addr": d["email"], "mail_pw": d["password"]}
print("newtmpuser {}: addr={}".format(index, config["addr"])) print("newtmpuser {}: addr={}".format(index, config["addr"]))
self._configlist.append(config) self._configlist.append(config)
yield config yield config
@@ -229,7 +229,7 @@ def write_dict_to_dir(dic, target_dir):
path.write_bytes(content) path.write_bytes(content)
@pytest.fixture @pytest.fixture()
def data(request): def data(request):
class Data: class Data:
def __init__(self) -> None: def __init__(self) -> None:
@@ -253,6 +253,7 @@ def data(request):
if os.path.exists(fn): if os.path.exists(fn):
return fn return fn
print("WARNING: path does not exist: {!r}".format(fn)) print("WARNING: path does not exist: {!r}".format(fn))
return None
def read_path(self, bn, mode="r"): def read_path(self, bn, mode="r"):
fn = self.get_path(bn) fn = self.get_path(bn)
@@ -264,8 +265,11 @@ def data(request):
class ACSetup: class ACSetup:
"""accounts setup helper to deal with multiple configure-process """
and io & imap initialization phases. From tests, use the higher level Accounts setup helper to deal with multiple configure-process
and io & imap initialization phases.
From tests, use the higher level
public ACFactory methods instead of its private helper class. public ACFactory methods instead of its private helper class.
""" """
@@ -289,7 +293,7 @@ class ACSetup:
self._account2state[account] = self.CONFIGURED self._account2state[account] = self.CONFIGURED
self.log("added already configured account", account, account.get_config("addr")) self.log("added already configured account", account, account.get_config("addr"))
def start_configure(self, account, reconfigure=False): def start_configure(self, account):
"""add an account and start its configure process.""" """add an account and start its configure process."""
class PendingTracker: class PendingTracker:
@@ -299,7 +303,7 @@ class ACSetup:
account.add_account_plugin(PendingTracker(), name="pending_tracker") account.add_account_plugin(PendingTracker(), name="pending_tracker")
self._account2state[account] = self.CONFIGURING self._account2state[account] = self.CONFIGURING
account.configure(reconfigure=reconfigure) account.configure()
self.log("started configure on", account) self.log("started configure on", account)
def wait_one_configured(self, account): def wait_one_configured(self, account):
@@ -411,7 +415,8 @@ class ACFactory:
acc.disable_logging() acc.disable_logging()
def get_next_liveconfig(self): def get_next_liveconfig(self):
"""Base function to get functional online configurations """
Base function to get functional online configurations
where we can make valid SMTP and IMAP connections with. where we can make valid SMTP and IMAP connections with.
""" """
configdict = next(self._liveconfig_producer).copy() configdict = next(self._liveconfig_producer).copy()
@@ -465,8 +470,7 @@ class ACFactory:
if fname_pub and fname_sec: if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec) account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True return True
else: print("WARN: could not use preconfigured keys for {!r}".format(addr))
print("WARN: could not use preconfigured keys for {!r}".format(addr))
def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account: def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account:
# do a pseudo-configured account # do a pseudo-configured account
@@ -476,14 +480,14 @@ class ACFactory:
acname = ac._logid acname = ac._logid
addr = "{}@offline.org".format(acname) addr = "{}@offline.org".format(acname)
ac.update_config( ac.update_config(
dict( {
addr=addr, "addr": addr,
displayname=acname, "displayname": acname,
mail_pw="123", "mail_pw": "123",
configured_addr=addr, "configured_addr": addr,
configured_mail_pw="123", "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)
@@ -494,12 +498,12 @@ class ACFactory:
configdict = self.get_next_liveconfig() configdict = self.get_next_liveconfig()
else: else:
# XXX we might want to transfer the key to the new account # XXX we might want to transfer the key to the new account
configdict = dict( configdict = {
addr=cloned_from.get_config("addr"), "addr": cloned_from.get_config("addr"),
mail_pw=cloned_from.get_config("mail_pw"), "mail_pw": cloned_from.get_config("mail_pw"),
imap_certificate_checks=cloned_from.get_config("imap_certificate_checks"), "imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
smtp_certificate_checks=cloned_from.get_config("smtp_certificate_checks"), "smtp_certificate_checks": cloned_from.get_config("smtp_certificate_checks"),
) }
configdict.update(kwargs) configdict.update(kwargs)
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
if ac is not None: if ac is not None:
@@ -600,7 +604,7 @@ class ACFactory:
acc._evtracker.wait_next_incoming_message() acc._evtracker.wait_next_incoming_message()
@pytest.fixture @pytest.fixture()
def acfactory(request, tmpdir, testprocess, data): def acfactory(request, tmpdir, testprocess, data):
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data) am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
yield am yield am
@@ -665,12 +669,12 @@ class BotProcess:
ignored.append(line) ignored.append(line)
@pytest.fixture @pytest.fixture()
def tmp_db_path(tmpdir): def tmp_db_path(tmpdir):
return tmpdir.join("test.db").strpath return tmpdir.join("test.db").strpath
@pytest.fixture @pytest.fixture()
def lp(): def lp():
class Printer: class Printer:
def sec(self, msg: str) -> None: def sec(self, msg: str) -> None:

View File

@@ -77,11 +77,11 @@ class ConfigureTracker:
self.account.remove_account_plugin(self) self.account.remove_account_plugin(self)
def wait_smtp_connected(self): def wait_smtp_connected(self):
"""wait until smtp is configured.""" """Wait until SMTP is configured."""
self._smtp_finished.wait() self._smtp_finished.wait()
def wait_imap_connected(self): def wait_imap_connected(self):
"""wait until smtp is configured.""" """Wait until IMAP is configured."""
self._imap_finished.wait() self._imap_finished.wait()
def wait_progress(self, data1=None): def wait_progress(self, data1=None):
@@ -91,7 +91,8 @@ class ConfigureTracker:
break break
def wait_finish(self, timeout=None): def wait_finish(self, timeout=None):
"""wait until configure is completed. """
Wait until configure is completed.
Raise Exception if Configure failed Raise Exception if Configure failed
""" """

View File

@@ -15,5 +15,5 @@ if __name__ == "__main__":
p, p,
"-w", "-w",
workspacedir, workspacedir,
] ],
) )

View File

@@ -216,7 +216,7 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
# would also find the "Sent" folder, but it would be too late: # would also find the "Sent" folder, but it would be too late:
# The sentbox thread, started by `start_io()`, would have seen that there is no # The sentbox thread, started by `start_io()`, would have seen that there is no
# ConfiguredSentboxFolder and do nothing. # ConfiguredSentboxFolder and do nothing.
acfactory._acsetup.start_configure(ac1, reconfigure=True) acfactory._acsetup.start_configure(ac1)
acfactory.bring_accounts_online() acfactory.bring_accounts_online()
assert_folders_configured(ac1) assert_folders_configured(ac1)

View File

@@ -32,7 +32,7 @@ def test_basic_imap_api(acfactory, tmpdir):
imap2.shutdown() imap2.shutdown()
@pytest.mark.ignored @pytest.mark.ignored()
def test_configure_generate_key(acfactory, lp): def test_configure_generate_key(acfactory, lp):
# A slow test which will generate new keys. # A slow test which will generate new keys.
acfactory.remove_preconfigured_keys() acfactory.remove_preconfigured_keys()
@@ -510,7 +510,7 @@ def test_send_and_receive_message_markseen(acfactory, lp):
idle2.wait_for_seen() idle2.wait_for_seen()
lp.step("1") lp.step("1")
for i in range(2): for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ") ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
@@ -529,7 +529,7 @@ def test_send_and_receive_message_markseen(acfactory, lp):
pass # mark_seen_messages() has generated events before it returns pass # mark_seen_messages() has generated events before it returns
def test_moved_markseen(acfactory, lp): def test_moved_markseen(acfactory):
"""Test that message already moved to DeltaChat folder is marked as seen.""" """Test that message already moved to DeltaChat folder is marked as seen."""
ac1 = acfactory.new_online_configuring_account() ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(mvbox_move=True) ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
@@ -553,7 +553,7 @@ def test_moved_markseen(acfactory, lp):
ac2.mark_seen_messages([msg]) ac2.mark_seen_messages([msg])
uid = idle2.wait_for_seen() uid = idle2.wait_for_seen()
assert len([a for a in ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*")))]) == 1 assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*"))))) == 1
def test_message_override_sender_name(acfactory, lp): def test_message_override_sender_name(acfactory, lp):
@@ -832,7 +832,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
lp.sec("sending multi-line non-unicode message from ac1 to ac2") lp.sec("sending multi-line non-unicode message from ac1 to ac2")
text1 = ( text1 = (
"hello\nworld\nthis is a very long message that should be" "hello\nworld\nthis is a very long message that should be"
+ " wrapped using format=flowed and unwrapped on the receiver" " wrapped using format=flowed and unwrapped on the receiver"
) )
msg_out = chat.send_text(text1) msg_out = chat.send_text(text1)
assert not msg_out.is_encrypted() assert not msg_out.is_encrypted()
@@ -894,7 +894,7 @@ def test_dont_show_emails(acfactory, lp):
message in Drafts that is moved to Sent later message in Drafts that is moved to Sent later
""".format( """.format(
ac1.get_config("configured_addr") ac1.get_config("configured_addr"),
), ),
) )
ac1.direct_imap.append( ac1.direct_imap.append(
@@ -908,7 +908,7 @@ def test_dont_show_emails(acfactory, lp):
message in Sent message in Sent
""".format( """.format(
ac1.get_config("configured_addr") ac1.get_config("configured_addr"),
), ),
) )
ac1.direct_imap.append( ac1.direct_imap.append(
@@ -922,7 +922,7 @@ def test_dont_show_emails(acfactory, lp):
Unknown message in Spam Unknown message in Spam
""".format( """.format(
ac1.get_config("configured_addr") ac1.get_config("configured_addr"),
), ),
) )
ac1.direct_imap.append( ac1.direct_imap.append(
@@ -936,7 +936,7 @@ def test_dont_show_emails(acfactory, lp):
Unknown & malformed message in Spam Unknown & malformed message in Spam
""".format( """.format(
ac1.get_config("configured_addr") ac1.get_config("configured_addr"),
), ),
) )
ac1.direct_imap.append( ac1.direct_imap.append(
@@ -950,7 +950,7 @@ def test_dont_show_emails(acfactory, lp):
Unknown & malformed message in Spam Unknown & malformed message in Spam
""".format( """.format(
ac1.get_config("configured_addr") ac1.get_config("configured_addr"),
), ),
) )
ac1.direct_imap.append( ac1.direct_imap.append(
@@ -964,7 +964,7 @@ def test_dont_show_emails(acfactory, lp):
Actually interesting message in Spam Actually interesting message in Spam
""".format( """.format(
ac1.get_config("configured_addr") ac1.get_config("configured_addr"),
), ),
) )
ac1.direct_imap.append( ac1.direct_imap.append(
@@ -978,7 +978,7 @@ def test_dont_show_emails(acfactory, lp):
Unknown message in Junk Unknown message in Junk
""".format( """.format(
ac1.get_config("configured_addr") ac1.get_config("configured_addr"),
), ),
) )
@@ -1710,7 +1710,7 @@ def test_system_group_msg_from_blocked_user(acfactory, lp):
assert contact.is_blocked() assert contact.is_blocked()
chat_on_ac2.remove_contact(ac1) chat_on_ac2.remove_contact(ac1)
ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED") ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
assert not ac1.get_self_contact() in chat_on_ac1.get_contacts() assert ac1.get_self_contact() not in chat_on_ac1.get_contacts()
def test_set_get_group_image(acfactory, data, lp): def test_set_get_group_image(acfactory, data, lp):
@@ -1784,7 +1784,7 @@ def test_connectivity(acfactory, lp):
lp.sec( lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " "Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
+ "all messages are fetched" "all messages are fetched",
) )
ac1.direct_imap.select_config_folder("inbox") ac1.direct_imap.select_config_folder("inbox")
@@ -2146,7 +2146,7 @@ def test_group_quote(acfactory, lp):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"folder,move,expected_destination,", ("folder", "move", "expected_destination"),
[ [
( (
"xyz", "xyz",
@@ -2298,7 +2298,7 @@ class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory): def test_invalid_password(self, acfactory):
configdict = acfactory.get_next_liveconfig() configdict = acfactory.get_next_liveconfig()
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()
ac1.update_config(dict(addr=configdict["addr"], mail_pw="123")) ac1.update_config({"addr": configdict["addr"], "mail_pw": "123"})
configtracker = ac1.configure() configtracker = ac1.configure()
configtracker.wait_progress(500) configtracker.wait_progress(500)
configtracker.wait_progress(0) configtracker.wait_progress(0)

View File

@@ -15,7 +15,7 @@ from deltachat.tracker import ImexFailed
@pytest.mark.parametrize( @pytest.mark.parametrize(
"msgtext,res", ("msgtext", "res"),
[ [
( (
"Member Me (tmp1@x.org) removed by tmp2@x.org.", "Member Me (tmp1@x.org) removed by tmp2@x.org.",
@@ -108,7 +108,7 @@ class TestOfflineAccountBasic:
def test_update_config(self, acfactory): def test_update_config(self, acfactory):
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()
ac1.update_config(dict(mvbox_move=False)) ac1.update_config({"mvbox_move": False})
assert ac1.get_config("mvbox_move") == "0" assert ac1.get_config("mvbox_move") == "0"
def test_has_savemime(self, acfactory): def test_has_savemime(self, acfactory):
@@ -229,11 +229,11 @@ class TestOfflineContact:
class TestOfflineChat: class TestOfflineChat:
@pytest.fixture @pytest.fixture()
def ac1(self, acfactory): def ac1(self, acfactory):
return acfactory.get_pseudo_configured_account() return acfactory.get_pseudo_configured_account()
@pytest.fixture @pytest.fixture()
def chat1(self, ac1): def chat1(self, ac1):
return ac1.create_contact("some1@example.org", name="some1").create_chat() return ac1.create_contact("some1@example.org", name="some1").create_chat()
@@ -257,7 +257,7 @@ class TestOfflineChat:
assert chat2.id == chat1.id assert chat2.id == chat1.id
assert chat2.get_name() == chat1.get_name() assert chat2.get_name() == chat1.get_name()
assert chat1 == chat2 assert chat1 == chat2
assert not (chat1 != chat2) assert not chat1.__ne__(chat2)
assert chat1 != chat3 assert chat1 != chat3
for ichat in ac1.get_chats(): for ichat in ac1.get_chats():
@@ -450,7 +450,7 @@ class TestOfflineChat:
assert msg.filemime == "image/png" assert msg.filemime == "image/png"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fn,typein,typeout", ("fn", "typein", "typeout"),
[ [
("r", None, "application/octet-stream"), ("r", None, "application/octet-stream"),
("r.txt", None, "text/plain"), ("r.txt", None, "text/plain"),
@@ -458,7 +458,7 @@ class TestOfflineChat:
("r.txt", "image/png", "image/png"), ("r.txt", "image/png", "image/png"),
], ],
) )
def test_message_file(self, ac1, chat1, data, lp, fn, typein, typeout): def test_message_file(self, chat1, data, lp, fn, typein, typeout):
lp.sec("sending file") lp.sec("sending file")
fp = data.get_path(fn) fp = data.get_path(fn)
msg = chat1.send_file(fp, typein) msg = chat1.send_file(fp, typein)
@@ -694,7 +694,7 @@ class TestOfflineChat:
chat1.set_draft(None) chat1.set_draft(None)
assert chat1.get_draft() is None assert chat1.get_draft() is None
def test_qr_setup_contact(self, acfactory, lp): def test_qr_setup_contact(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account() ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account() ac2 = acfactory.get_pseudo_configured_account()
qr = ac1.get_setup_contact_qr() qr = ac1.get_setup_contact_qr()

View File

@@ -93,7 +93,7 @@ def test_empty_context():
capi.lib.dc_context_unref(ctx) capi.lib.dc_context_unref(ctx)
def test_dc_close_events(tmpdir, acfactory): def test_dc_close_events(acfactory):
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()
# register after_shutdown function # register after_shutdown function

View File

@@ -50,18 +50,14 @@ commands =
skipsdist = True skipsdist = True
skip_install = True skip_install = True
deps = deps =
flake8 ruff
# isort 5.11.0 is broken: https://github.com/PyCQA/isort/issues/2031
isort<5.11.0
black black
# pygments required by rst-lint # pygments required by rst-lint
pygments pygments
restructuredtext_lint restructuredtext_lint
commands = commands =
isort --check setup.py install_python_bindings.py src/deltachat examples/ tests/
black --check setup.py install_python_bindings.py src/deltachat examples/ tests/ black --check setup.py install_python_bindings.py src/deltachat examples/ tests/
flake8 src/deltachat ruff src/deltachat tests/ examples/
flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst rst-lint --encoding 'utf-8' README.rst
[testenv:mypy] [testenv:mypy]
@@ -102,7 +98,3 @@ timeout = 150
timeout_func_only = True timeout_func_only = True
markers = markers =
ignored: ignore this test in default test runs, use --ignored to run. ignored: ignore this test in default test runs, use --ignored to run.
[flake8]
max-line-length = 120
ignore = E203, E266, E501, W503

View File

@@ -2,12 +2,13 @@
Remove old "dc" indices except for master which always stays. Remove old "dc" indices except for master which always stays.
""" """
from requests import Session
import datetime import datetime
import sys
import subprocess import subprocess
import sys
MAXDAYS=7 from requests import Session
MAXDAYS = 7
session = Session() session = Session()
session.headers["Accept"] = "application/json" session.headers["Accept"] = "application/json"
@@ -54,7 +55,8 @@ def run():
if not dates: if not dates:
print( print(
"%s has no releases" % (baseurl + username + "/" + indexname), "%s has no releases" % (baseurl + username + "/" + indexname),
file=sys.stderr) file=sys.stderr,
)
date = datetime.datetime.now() date = datetime.datetime.now()
else: else:
date = datetime.datetime(*max(dates)) date = datetime.datetime(*max(dates))
@@ -67,6 +69,5 @@ def run():
subprocess.check_call(["devpi", "index", "-y", "--delete", url]) subprocess.check_call(["devpi", "index", "-y", "--delete", url])
if __name__ == "__main__":
if __name__ == '__main__':
run() run()

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import json import json
import re import os
import pathlib import pathlib
import re
import subprocess import subprocess
from argparse import ArgumentParser from argparse import ArgumentParser
@@ -23,7 +23,7 @@ def read_toml_version(relpath):
res = regex_matches(relpath, rex) res = regex_matches(relpath, rex)
if res is not None: if res is not None:
return res.group(1) return res.group(1)
raise ValueError("no version found in {}".format(relpath)) raise ValueError(f"no version found in {relpath}")
def replace_toml_version(relpath, newversion): def replace_toml_version(relpath, newversion):
@@ -34,8 +34,8 @@ def replace_toml_version(relpath, newversion):
for line in open(str(p)): for line in open(str(p)):
m = rex.match(line) m = rex.match(line)
if m is not None: if m is not None:
print("{}: set version={}".format(relpath, newversion)) print(f"{relpath}: set version={newversion}")
f.write('version = "{}"\n'.format(newversion)) f.write(f'version = "{newversion}"\n')
else: else:
f.write(line) f.write(line)
os.rename(tmp_path, str(p)) os.rename(tmp_path, str(p))
@@ -44,7 +44,7 @@ def replace_toml_version(relpath, newversion):
def read_json_version(relpath): def read_json_version(relpath):
p = pathlib.Path(relpath) p = pathlib.Path(relpath)
assert p.exists() assert p.exists()
with open(p, "r") as f: with open(p) as f:
json_data = json.loads(f.read()) json_data = json.loads(f.read())
return json_data["version"] return json_data["version"]
@@ -52,7 +52,7 @@ def read_json_version(relpath):
def update_package_json(relpath, newversion): def update_package_json(relpath, newversion):
p = pathlib.Path(relpath) p = pathlib.Path(relpath)
assert p.exists() assert p.exists()
with open(p, "r") as f: with open(p) as f:
json_data = json.loads(f.read()) json_data = json.loads(f.read())
json_data["version"] = newversion json_data["version"] = newversion
with open(p, "w") as f: with open(p, "w") as f:
@@ -63,7 +63,7 @@ def main():
parser = ArgumentParser(prog="set_core_version") parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion") parser.add_argument("newversion")
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"] json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
toml_list = [ toml_list = [
"Cargo.toml", "Cargo.toml",
"deltachat-ffi/Cargo.toml", "deltachat-ffi/Cargo.toml",
@@ -75,9 +75,9 @@ def main():
except SystemExit: except SystemExit:
print() print()
for x in toml_list: for x in toml_list:
print("{}: {}".format(x, read_toml_version(x))) print(f"{x}: {read_toml_version(x)}")
for x in json_list: for x in json_list:
print("{}: {}".format(x, read_json_version(x))) print(f"{x}: {read_json_version(x)}")
print() print()
raise SystemExit("need argument: new version, example: 1.25.0") raise SystemExit("need argument: new version, example: 1.25.0")
@@ -92,19 +92,19 @@ def main():
if "alpha" not in newversion: if "alpha" not in newversion:
for line in open("CHANGELOG.md"): for line in open("CHANGELOG.md"):
## 1.25.0 ## 1.25.0
if line.startswith("## "): if line.startswith("## ") and line[2:].strip().startswith(newversion):
if line[2:].strip().startswith(newversion): break
break
else: else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion)) raise SystemExit(
f"CHANGELOG.md contains no entry for version: {newversion}"
)
for toml_filename in toml_list: for toml_filename in toml_list:
replace_toml_version(toml_filename, newversion) replace_toml_version(toml_filename, newversion)
for json_filename in json_list: for json_filename in json_list:
update_package_json(json_filename, newversion) update_package_json(json_filename, newversion)
print("running cargo check") print("running cargo check")
subprocess.call(["cargo", "check"]) subprocess.call(["cargo", "check"])
@@ -114,13 +114,12 @@ def main():
print("after commit, on master make sure to: ") print("after commit, on master make sure to: ")
print("") print("")
print(" git tag -a {}".format(newversion)) print(f" git tag -a {newversion}")
print(" git push origin {}".format(newversion)) print(f" git push origin {newversion}")
print(" git tag -a py-{}".format(newversion)) print(f" git tag -a py-{newversion}")
print(" git push origin py-{}".format(newversion)) print(f" git push origin py-{newversion}")
print("") print("")
if __name__ == "__main__": if __name__ == "__main__":
main() main()