diff --git a/src/backend/backend.py b/src/backend/backend.py
index 12f95591..762aa3fe 100644
--- a/src/backend/backend.py
+++ b/src/backend/backend.py
@@ -13,11 +13,10 @@ from typing import Any, DefaultDict, Dict, List, Optional, Tuple, Union
from urllib.parse import urlparse
import aiohttp
+import nio
import plyer
from appdirs import AppDirs
-import nio
-
from . import __app_name__
from .errors import MatrixError, MatrixInvalidAccessToken
from .matrix_client import MatrixClient
@@ -29,7 +28,7 @@ from .models.model import Model
from .models.model_store import ModelStore
from .presence import Presence
from .sso_server import SSOServer
-from .user_files import Accounts, History, Theme, Settings, UIState
+from .user_files import Accounts, History, Settings, Theme, UIState
# Logging configuration
log.getLogger().setLevel(log.INFO)
@@ -195,7 +194,7 @@ class Backend:
async def password_auth(
self, user: str, password: str, homeserver: str,
- ) -> str:
+ ) -> str:
"""Create & register a `MatrixClient`, login using the password
and return the user ID we get.
"""
@@ -437,7 +436,9 @@ class Backend:
)
- async def set_string_filter(self, model_id: SyncId, value: str) -> None:
+ async def set_string_filter(
+ self, model_id: Union[SyncId, List[str]], value: str,
+ ) -> None:
"""Set a FieldStringFilter (or derived class) model's filter property.
This should only be called from QML.
@@ -475,7 +476,7 @@ class Backend:
try:
await session.get(f"{homeserver_url}/_matrix/client/versions")
- except Exception as err:
+ except aiohttp.ClientError as err:
log.warning("Failed pinging %s: %r", homeserver_url, err)
item.status = PingStatus.Failed
return
diff --git a/src/backend/html_markdown.py b/src/backend/html_markdown.py
index 8ccb0502..108bb098 100644
--- a/src/backend/html_markdown.py
+++ b/src/backend/html_markdown.py
@@ -10,11 +10,10 @@ from urllib.parse import unquote
import html_sanitizer.sanitizer as sanitizer
import lxml.html # nosec
import mistune
+import nio
from html_sanitizer.sanitizer import Sanitizer
from lxml.html import HtmlElement, etree # nosec
-import nio
-
from .svg_colors import SVG_COLORS
@@ -113,7 +112,7 @@ class HTMLProcessor:
}
block_tags = {
- "h1", "h2", "h3", "h4", "h5", "h6","blockquote",
+ "h1", "h2", "h3", "h4", "h5", "h6", "blockquote",
"p", "ul", "ol", "li", "hr", "br", "img",
"table", "thead", "tbody", "tr", "th", "td", "pre",
"mx-reply",
diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py
index 921935cb..56c0c918 100644
--- a/src/backend/matrix_client.py
+++ b/src/backend/matrix_client.py
@@ -25,14 +25,12 @@ from typing import (
from urllib.parse import urlparse
from uuid import UUID, uuid4
-import aiofiles
import cairosvg
-from PIL import Image as PILImage
-from pymediainfo import MediaInfo
-
import nio
from nio.crypto import AsyncDataT as UploadData
from nio.crypto import async_generator_from_data
+from PIL import Image as PILImage
+from pymediainfo import MediaInfo
from . import __display_name__, __reverse_dns__, utils
from .errors import (
@@ -69,16 +67,18 @@ PathCallable = Union[
IS_WINDOWS = platform.system() == "Windows"
+MATRIX_TO = "https://matrix.to/#"
+
REPLY_FALLBACK = (
-""
- ""
- 'In reply to '
- '{user_id}'
- "
"
- "{content}"
- "
"
-""
-"{reply_content}"
+ ""
+ ""
+ 'In reply to '
+ '{user_id}'
+ "
"
+ "{content}"
+ "
"
+ ""
+ "{reply_content}"
)
@@ -88,6 +88,7 @@ class SyncFilterIds(NamedTuple):
first: str
others: str
+
class UploadReturn(NamedTuple):
"""Details for an uploaded file."""
@@ -161,10 +162,10 @@ class MatrixClient(nio.AsyncClient):
def __init__(
self,
- backend,
- user: str = "",
- homeserver: str = "https://matrix.org",
- device_id: Optional[str] = None,
+ backend,
+ user: str = "",
+ homeserver: str = "https://matrix.org",
+ device_id: Optional[str] = None,
) -> None:
store = Path(backend.appdirs.user_data_dir) / "encryption"
@@ -364,7 +365,7 @@ class MatrixClient(nio.AsyncClient):
try:
account.max_upload_size = future.result() or 0
- except Exception:
+ except MatrixError:
trace = traceback.format_exc().rstrip()
log.warn(
"On %s server config retrieval: %s", self.user_id, trace,
@@ -403,8 +404,9 @@ class MatrixClient(nio.AsyncClient):
sync_filter = sync_filter_ids.others,
))
await self.sync_task
+ self.last_sync_error = None
break # task cancelled
- except Exception as err:
+ except Exception as err: # noqa
self.last_sync_error = err
trace = traceback.format_exc().rstrip()
@@ -417,8 +419,6 @@ class MatrixClient(nio.AsyncClient):
)
else:
LoopException(str(err), err, trace)
- else:
- self.last_sync_error = None
await asyncio.sleep(5)
@@ -562,7 +562,7 @@ class MatrixClient(nio.AsyncClient):
if text.startswith("/me ") and not escape:
event_type = nio.RoomMessageEmote
- text = text[len("/me "): ]
+ text = text[len("/me "):]
content = {"body": text, "msgtype": "m.emote"}
to_html = from_md(text, inline=True, outgoing=True)
echo_body = from_md(text, inline=True)
@@ -586,14 +586,15 @@ class MatrixClient(nio.AsyncClient):
plain_source_body = "\n".join(
f"> <{to.sender_id}> {line}" if i == 0 else f"> {line}"
for i, line in enumerate(source_body.splitlines())
- )
+ )
content["body"] = f"{plain_source_body}\n\n{text}"
to_html = REPLY_FALLBACK.format(
- room_id = room_id,
- event_id = to.event_id,
- user_id = to.sender_id,
- content =
+ matrix_to = MATRIX_TO,
+ room_id = room_id,
+ event_id = to.event_id,
+ user_id = to.sender_id,
+ content =
getattr(to.source, "formatted_body", "") or
source_body or
html.escape(to.source.source["type"] if to.source else ""),
@@ -605,7 +606,7 @@ class MatrixClient(nio.AsyncClient):
content["formatted_body"] = HTML.filter(to_html, outgoing=True)
content["m.relates_to"] = {
- "m.in_reply_to": { "event_id": to.event_id },
+ "m.in_reply_to": {"event_id": to.event_id},
}
# Can't use the standard Matrix transaction IDs; they're only visible
@@ -662,7 +663,7 @@ class MatrixClient(nio.AsyncClient):
# optimize is too slow for large images
compressed = await utils.compress_image(image, optimize=False)
- async with aiofiles.open(temp.name, "wb") as file:
+ async with utils.aiopen(temp.name, "wb") as file:
await file.write(compressed)
return Path(temp.name)
@@ -814,7 +815,7 @@ class MatrixClient(nio.AsyncClient):
)
except UneededThumbnail:
pass
- except Exception:
+ except Exception: # noqa
trace = traceback.format_exc().rstrip()
log.warning("Failed thumbnailing %s:\n%s", path, trace)
else:
@@ -1366,7 +1367,8 @@ class MatrixClient(nio.AsyncClient):
event.set_fields(
content = await self.get_redacted_event_content(
- event.event_type, self.user_id, event.sender_id,reason,
+ event.event_type, self.user_id, event.sender_id,
+ reason,
),
event_type = nio.RedactedEvent,
@@ -1568,7 +1570,7 @@ class MatrixClient(nio.AsyncClient):
account.status_msg = status_msg
await super().set_presence(
- "offline" if presence == "invisible" else presence,
+ "offline" if presence == "invisible" else presence,
status_msg,
)
@@ -2070,7 +2072,7 @@ class MatrixClient(nio.AsyncClient):
image_data = None
create = False
- async with aiofiles.open(avatar_path, "rb") as file:
+ async with utils.aiopen(avatar_path, "rb") as file:
if await utils.is_svg(file):
await file.seek(0, 0)
diff --git a/src/backend/media_cache.py b/src/backend/media_cache.py
index aedad1bb..28136c2e 100644
--- a/src/backend/media_cache.py
+++ b/src/backend/media_cache.py
@@ -14,9 +14,8 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional
from urllib.parse import urlparse
-from PIL import Image as PILImage
-
import nio
+from PIL import Image as PILImage
from .utils import Size, atomic_write
diff --git a/src/backend/models/items.py b/src/backend/models/items.py
index 912c040a..99054621 100644
--- a/src/backend/models/items.py
+++ b/src/backend/models/items.py
@@ -11,7 +11,6 @@ from typing import Any, Dict, List, Optional, Tuple, Type, Union
from uuid import UUID
import lxml # nosec
-
import nio
from ..presence import Presence
diff --git a/src/backend/models/model_store.py b/src/backend/models/model_store.py
index 2063b1f9..67159740 100644
--- a/src/backend/models/model_store.py
+++ b/src/backend/models/model_store.py
@@ -3,7 +3,7 @@
from collections import UserDict
from dataclasses import dataclass, field
-from typing import Dict
+from typing import Dict, List, Union
from . import SyncId
from .model import Model
@@ -47,7 +47,7 @@ class ModelStore(UserDict):
elif is_tuple and len(key) == 3 and key[2] == "autocompleted_members":
model = AutoCompletedMembers(user_id=key[0], room_id=key[1])
else:
- model = Model(sync_id=key) # type: ignore
+ model = Model(sync_id=key)
self.data[key] = model
return model
@@ -62,7 +62,9 @@ class ModelStore(UserDict):
)
- async def ensure_exists_from_qml(self, sync_id: SyncId) -> None:
+ async def ensure_exists_from_qml(
+ self, sync_id: Union[SyncId, List[str]],
+ ) -> None:
"""Create model if it doesn't exist. Should only be called by QML."""
if isinstance(sync_id, list): # QML can't pass tuples
diff --git a/src/backend/models/special_models.py b/src/backend/models/special_models.py
index baa52fab..75d65759 100644
--- a/src/backend/models/special_models.py
+++ b/src/backend/models/special_models.py
@@ -40,7 +40,7 @@ class AllRooms(FieldSubstringFilter):
return source.sync_id == "accounts" or (
isinstance(source.sync_id, tuple) and
len(source.sync_id) == 2 and
- source.sync_id[1] == "rooms" # type: ignore
+ source.sync_id[1] == "rooms"
)
diff --git a/src/backend/nio_callbacks.py b/src/backend/nio_callbacks.py
index d8f790e6..ee2f5c27 100644
--- a/src/backend/nio_callbacks.py
+++ b/src/backend/nio_callbacks.py
@@ -366,9 +366,9 @@ class NioCallbacks:
def lvl(level: int) -> str:
return (
- f"Admin ({level})" if level == 100 else
+ f"Admin ({level})" if level == 100 else
f"Moderator ({level})" if level >= 50 else
- f"User ({level})" if level >= 0 else
+ f"User ({level})" if level >= 0 else
f"Muted ({level})"
)
@@ -788,8 +788,9 @@ class NioCallbacks:
datetime.now() - timedelta(milliseconds=ev.last_active_ago)
) if ev.last_active_ago else datetime.fromtimestamp(0)
- presence.presence = Presence.State(ev.presence) if ev.presence\
- else Presence.State.offline
+ presence.presence = \
+ Presence.State(ev.presence) if ev.presence else \
+ Presence.State.offline
# Add all existing members related to this presence
for room_id in self.models[self.user_id, "rooms"]:
@@ -818,9 +819,9 @@ class NioCallbacks:
# Set status_msg if none is set on the server and we have one
if (
- not presence.status_msg and
- account.status_msg and
- ev.user_id in self.client.backend.clients and
+ not presence.status_msg and
+ account.status_msg and
+ ev.user_id in self.client.backend.clients and
account.presence != Presence.State.echo_invisible and
presence.presence == Presence.State.offline
):
diff --git a/src/backend/pcn/section.py b/src/backend/pcn/section.py
index fbff7fb0..dda10b4f 100644
--- a/src/backend/pcn/section.py
+++ b/src/backend/pcn/section.py
@@ -364,7 +364,7 @@ class Section(MutableMapping):
for name in child.inherit_from.dumps().split(","):
name = name.strip()
- if root_arg is not None and name:
+ if name:
child_inherit.append(type(attrgetter(name)(root_arg)))
instance._set_section(section.from_source_code(
diff --git a/src/backend/qml_bridge.py b/src/backend/qml_bridge.py
index c3f94f5e..0ec9d3f9 100644
--- a/src/backend/qml_bridge.py
+++ b/src/backend/qml_bridge.py
@@ -89,7 +89,7 @@ class QMLBridge:
try:
result = future.result()
- except Exception as err:
+ except Exception as err: # noqa
exception = err
trace = traceback.format_exc().rstrip()
@@ -173,7 +173,7 @@ class QMLBridge:
asyncio.run_coroutine_threadsafe(
self.backend.terminate_clients(), self._loop,
).result()
- except Exception as e:
+ except Exception as e: # noqa
print(e)
diff --git a/src/backend/sso_server.py b/src/backend/sso_server.py
index 2157fc27..d5ecde36 100644
--- a/src/backend/sso_server.py
+++ b/src/backend/sso_server.py
@@ -4,6 +4,7 @@
import asyncio
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, quote, urlparse
+
from . import __display_name__
_SUCCESS_HTML_PAGE = """
diff --git a/src/backend/user_files.py b/src/backend/user_files.py
index a8eb2ff5..39961210 100644
--- a/src/backend/user_files.py
+++ b/src/backend/user_files.py
@@ -14,15 +14,15 @@ from typing import (
TYPE_CHECKING, Any, ClassVar, Dict, Iterator, Optional, Tuple,
)
-import aiofiles
-from watchgod import Change, awatch
-
import pyotherside
+from watchgod import Change, awatch
from .pcn.section import Section
from .pyotherside_events import LoopException, UserFileChanged
from .theme_parser import convert_to_qml
-from .utils import atomic_write, deep_serialize_for_qml, dict_update_recursive
+from .utils import (
+ aiopen, atomic_write, deep_serialize_for_qml, dict_update_recursive,
+)
if TYPE_CHECKING:
from .backend import Backend
@@ -122,7 +122,7 @@ class UserFile:
ignored += 1
continue
- async with aiofiles.open(self.path) as file:
+ async with aiopen(self.path) as file:
text = await file.read()
self.data, save = self.deserialized(text)
@@ -138,7 +138,7 @@ class UserFile:
if changes and ignored < len(changes):
UserFileChanged(type(self), self.qml_data)
- except Exception as err:
+ except Exception as err: # noqa
LoopException(str(err), err, traceback.format_exc().rstrip())
@@ -159,7 +159,7 @@ class UserFile:
self._need_write = False
self._mtime = self.write_path.stat().st_mtime
- except Exception as err:
+ except Exception as err: # noqa
self._need_write = False
LoopException(str(err), err, traceback.format_exc().rstrip())
diff --git a/src/backend/utils.py b/src/backend/utils.py
index 767363a3..cb4bb5fd 100644
--- a/src/backend/utils.py
+++ b/src/backend/utils.py
@@ -10,7 +10,7 @@ import inspect
import io
import json
import sys
-import xml.etree.cElementTree as xml_etree # FIXME: bandit warning
+import xml.etree.cElementTree as xml_etree
from concurrent.futures import ProcessPoolExecutor
from contextlib import suppress
from datetime import date, datetime, time, timedelta
@@ -27,8 +27,7 @@ from uuid import UUID
import aiofiles
import filetype
-from aiofiles.threadpool.binary import AsyncBufferedReader
-from aiofiles.threadpool.text import AsyncTextIOWrapper
+from aiofiles.threadpool.binary import AsyncBufferedIOBase
from nio.crypto import AsyncDataT as File
from nio.crypto import async_generator_from_data
from PIL import Image as PILImage
@@ -38,10 +37,9 @@ if sys.version_info >= (3, 7):
else:
from async_generator import asynccontextmanager
-AsyncOpenFile = Union[AsyncTextIOWrapper, AsyncBufferedReader]
-Size = Tuple[int, int]
-BytesOrPIL = Union[bytes, PILImage.Image]
-auto = autostr
+Size = Tuple[int, int]
+BytesOrPIL = Union[bytes, PILImage.Image]
+auto = autostr
COMPRESSION_POOL = ProcessPoolExecutor()
@@ -114,8 +112,8 @@ async def guess_mime(file: File) -> str:
if isinstance(file, io.IOBase):
file.seek(0, 0)
- elif isinstance(file, AsyncBufferedReader):
- await file.seek(0, 0)
+ elif isinstance(file, AsyncBufferedIOBase):
+ await file.seek(0, 0) # type: ignore
try:
first_chunk: bytes
@@ -134,8 +132,8 @@ async def guess_mime(file: File) -> str:
finally:
if isinstance(file, io.IOBase):
file.seek(0, 0)
- elif isinstance(file, AsyncBufferedReader):
- await file.seek(0, 0)
+ elif isinstance(file, AsyncBufferedIOBase):
+ await file.seek(0, 0) # type: ignore
def plain2html(text: str) -> str:
@@ -250,10 +248,17 @@ def classes_defined_in(module: ModuleType) -> Dict[str, Type]:
}
+@asynccontextmanager
+async def aiopen(*args, **kwargs) -> AsyncIterator[Any]:
+ """Wrapper for `aiofiles.open()` that doesn't break mypy"""
+ async with aiofiles.open(*args, **kwargs) as file:
+ yield file
+
+
@asynccontextmanager
async def atomic_write(
path: Union[Path, str], binary: bool = False, **kwargs,
-) -> AsyncIterator[Tuple[AsyncOpenFile, Callable[[], None]]]:
+) -> AsyncIterator[Tuple[Any, Callable[[], None]]]:
"""Write a file asynchronously (using aiofiles) and atomically.
Yields a `(open_temporary_file, done_function)` tuple.
@@ -279,7 +284,7 @@ async def atomic_write(
can_replace = True
try:
- async with aiofiles.open(temp_path, mode, **kwargs) as out:
+ async with aiopen(temp_path, mode, **kwargs) as out:
yield (out, done)
finally:
if can_replace: