aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2023-05-02 03:00:59 +0200
committerHugo Hörnquist <hugo@lysator.liu.se>2023-05-02 03:00:59 +0200
commitd17967eea2e42466103347257002451f17b5d5e8 (patch)
treed299b451ade590ab3cbf90c703c5914cad2fef4e
parentHopefully fix installation issues. (diff)
downloadmu4web-d17967eea2e42466103347257002451f17b5d5e8.tar.gz
mu4web-d17967eea2e42466103347257002451f17b5d5e8.tar.xz
Configure linters.
Introduce flake8 and mypy for better error checking. Fix all errors and warnings emitted by those. Also fix type error with `url_for`.
-rw-r--r--Makefile6
-rw-r--r--mu4web/maildir.py7
-rw-r--r--mu4web/main.py8
-rw-r--r--mu4web/mu.py70
-rwxr-xr-xmu4web/password.py2
-rw-r--r--mu4web/user/__init__.py8
-rw-r--r--mu4web/util.py9
-rw-r--r--mu4web/xapian.py6
-rw-r--r--setup.cfg10
9 files changed, 87 insertions, 39 deletions
diff --git a/Makefile b/Makefile
index b615b65..4ba0066 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,8 @@
-.PHONY: tags
+.PHONY: tags check
tags:
ctags `find mu4web -type f -name \*.py `
+
+check:
+ mypy -p mu4web
+ flake8 mu4web
diff --git a/mu4web/maildir.py b/mu4web/maildir.py
index c2abc16..dd5be1c 100644
--- a/mu4web/maildir.py
+++ b/mu4web/maildir.py
@@ -6,6 +6,8 @@ from .util import find
from urllib.parse import urlencode
+from pathlib import Path
+
from typing import (
Union
)
@@ -47,7 +49,7 @@ def _build_tree(items: list[list[str]]) -> MaildirGroup:
return node
-def find_maildirs(basedir) -> MaildirGroup:
+def find_maildirs(basedir: str) -> MaildirGroup:
"""
Find all maildirs located under basedir.
@@ -59,7 +61,7 @@ def find_maildirs(basedir) -> MaildirGroup:
and a group of maildirs then it will have two separate entries.
"""
basedir = basedir.rstrip('/')
- files = find(basedir, type='d', name='cur')
+ files = find(Path(basedir), type='d', name='cur')
# + 1 removes leading slash
# - 4 removes '/cur'
dirs = [entry[len(basedir) + 1:-4].decode('UTF-8').split(os.path.sep)
@@ -72,6 +74,7 @@ def serialize_maildir(maildir: MaildirGroup, path: list[str] = []) -> HTML:
"""Build a (recursive) list from a maildir node."""
entries: list[HTML] = []
for node in natsorted(maildir.children, key=lambda n: n.name):
+ entry: HTML
if isinstance(node, MaildirEntry):
parts = '/'.join(path + [node.name])
url = 'search?' + urlencode({'q': f'maildir:"/{parts}"'})
diff --git a/mu4web/main.py b/mu4web/main.py
index c147ab7..5d2d975 100644
--- a/mu4web/main.py
+++ b/mu4web/main.py
@@ -149,7 +149,7 @@ def page_base(title: Optional[str] = None,
('meta', {'name': 'viewport',
'content': 'width=device-width, initial-scale=0.5'}),
('title', title, ' — Mu4Web'),
- include_stylesheet(url_for('static', 'style.css')),
+ include_stylesheet(url_for('static', filename='style.css')),
),
('body',
('nav',
@@ -420,7 +420,7 @@ def attachement_response(attachement: EmailMessage):
Gets content type and encoding from the attachements headers.
"""
response = flask.Response()
- response.charset = attachement.get_content_charset()
+ response.charset = attachement.get_content_charset() or 'application/binary'
response.mimetype = attachement.get_content_type()
# does get_content do stuff depending on content-type?
# Check how to explicitly get the raw bytes.
@@ -500,7 +500,7 @@ class IMGParser(HTMLParser):
self.result += f' {key}="{data}"'
key = 'src'
- data = url_for('static', 'content-blocked.svg')
+ data = url_for('static', filename='content-blocked.svg')
self.result += f' {key}="{data}"'
else:
key = html.escape(key)
@@ -571,7 +571,7 @@ def attachement_part_page():
# above, which unblocks it.
# TODO this "fails" for images wrapped in anchor tags, since
# the anchor tag has priority.
- url = url_for('static', 'enable_images.js')
+ url = url_for('static', filename='enable_images.js')
result += f"\n<script src='{url}'></script>"
return str(result)
diff --git a/mu4web/mu.py b/mu4web/mu.py
index d75d7a9..8efbd38 100644
--- a/mu4web/mu.py
+++ b/mu4web/mu.py
@@ -11,17 +11,20 @@ import xml.dom.minidom
import xml.dom
from xdg.BaseDirectory import xdg_cache_home
import os.path
-import xapian
+from . import xapian
from datetime import datetime
+from os import PathLike
+from pathlib import Path
from typing import (
Optional,
+ TypedDict,
)
parser = BytesParser(policy=email.policy.default)
-def find_file(id: str) -> Optional[str]:
+def find_file(id: str) -> Optional[PathLike]:
cmd = subprocess.run(['mu', 'find', '-u', f'i:{id}',
'--fields', 'l'],
stdout=PIPE)
@@ -31,7 +34,7 @@ def find_file(id: str) -> Optional[str]:
return None
if cmd.returncode != 0:
raise MuError(cmd.returncode)
- return filename
+ return Path(filename)
def get_mail(id: str) -> email.message.Message:
@@ -41,7 +44,11 @@ def get_mail(id: str) -> email.message.Message:
[Raises]
MuError
"""
- with open(find_file(id), "rb") as f:
+ filename = find_file(id)
+ if not filename:
+ # TODO better error here
+ raise MuError(1)
+ with open(filename, "rb") as f:
mail = parser.parse(f)
return mail
@@ -57,10 +64,10 @@ class MuError(Exception):
self.returncode: int = returncode
self.msg: str = MuError.codes.get(returncode, 'Unknown Error')
- def __repr__(self):
+ def __repr__(self) -> str:
return f'MuError({self.returncode}, "{self.msg}")'
- def __str__(self):
+ def __str__(self) -> str:
return repr(self)
@@ -118,7 +125,7 @@ def search(query: str,
return message_list
-def base_directory():
+def base_directory() -> PathLike:
"""
Returns where where mu stores its files.
@@ -127,28 +134,47 @@ def base_directory():
TODO make this configurable.
"""
- return os.getenv('MUHOME') or os.path.join(xdg_cache_home, 'mu')
+ return Path(os.getenv('MUHOME') or os.path.join(xdg_cache_home, 'mu'))
-def info():
+class MuInfo(TypedDict):
+ database_path: str
+ changed: datetime
+ created: datetime
+ indexed: datetime
+ maildir: str
+ schema_version: str
+ messages_in_store: int
+
+
+def info() -> MuInfo:
db = os.path.join(base_directory(), "xapian")
- info = {
- 'database-path': db,
- }
+ def f(key: str) -> datetime:
+ return datetime.fromtimestamp(int(xapian.metadata_get(db, key), 16))
- for key in ['changed', 'created', 'indexed']:
- info[key] = datetime.fromtimestamp(int(xapian.metadata_get(db, key), 16))
+ changed = f('changed')
+ created = f('created')
+ indexed = f('indexed')
- for key in ['maildir', 'schema-version']:
- info[key] = xapian.metadata_get(db, key)
+ maildir = xapian.metadata_get(db, 'maildir')
+ schema_version = xapian.metadata_get(db, 'schema-version')
- cmd = subprocess.Popen(['xapian-delve', '-V3', '-1', db], stdout=subprocess.PIPE)
+ cmd = subprocess.Popen(['xapian-delve', '-V3', '-1', db],
+ stdout=subprocess.PIPE)
# Start at minus one to ignore header
count = -1
- for line in cmd.stdout:
- count += 1
- info['messages-in-store'] = count
-
- return info
+ if cmd.stdout:
+ for line in cmd.stdout:
+ count += 1
+
+ return {
+ 'database_path': db,
+ 'messages_in_store': count,
+ 'changed': changed,
+ 'created': created,
+ 'indexed': indexed,
+ 'maildir': maildir,
+ 'schema_version': schema_version,
+ }
diff --git a/mu4web/password.py b/mu4web/password.py
index af33cb6..26bc712 100755
--- a/mu4web/password.py
+++ b/mu4web/password.py
@@ -89,7 +89,7 @@ class Passwords:
return data['hash'] == digest
-def main():
+def main() -> None:
import argparse
parser = argparse.ArgumentParser()
diff --git a/mu4web/user/__init__.py b/mu4web/user/__init__.py
index 1ecc49f..bb14f67 100644
--- a/mu4web/user/__init__.py
+++ b/mu4web/user/__init__.py
@@ -3,16 +3,16 @@ class User:
self._username = username
self._authenticated = False
- def is_authenticated(self):
+ def is_authenticated(self) -> bool:
return self._authenticated
- def is_active(self):
+ def is_active(self) -> bool:
return True
- def is_anonymous(self):
+ def is_anonymous(self) -> bool:
return False
- def get_id(self):
+ def get_id(self) -> str:
return self._username
def validate(self, _: str) -> bool:
diff --git a/mu4web/util.py b/mu4web/util.py
index c8ccc7d..41601c5 100644
--- a/mu4web/util.py
+++ b/mu4web/util.py
@@ -1,8 +1,13 @@
import subprocess
+from os import PathLike
+from typing import (
+ Union,
+)
-def find(basedir, **flags) -> list[bytes]:
- cmdline = ['find', basedir]
+def find(basedir: PathLike,
+ **flags: str | bytes) -> list[bytes]:
+ cmdline: list[Union[str, bytes, PathLike]] = ['find', basedir]
for key, value in flags.items():
cmdline += [f'-{key}', value]
cmdline.append('-print0')
diff --git a/mu4web/xapian.py b/mu4web/xapian.py
index 8aebf72..d23b2d1 100644
--- a/mu4web/xapian.py
+++ b/mu4web/xapian.py
@@ -1,7 +1,8 @@
import subprocess
from typing import Optional
-def metadata_list(db, prefix: Optional[str] = None) -> list[str]:
+
+def metadata_list(db: str, prefix: Optional[str] = None) -> list[str]:
cmdline = ['xapian-metadata', 'list', db]
if prefix:
cmdline.append(prefix)
@@ -9,8 +10,7 @@ def metadata_list(db, prefix: Optional[str] = None) -> list[str]:
return cmd.stdout.split('\n')
-def metadata_get(db, key: str) -> str:
+def metadata_get(db: str, key: str) -> str:
cmd = subprocess.run(['xapian-metadata', 'get', db, key],
capture_output=True, text=True)
return cmd.stdout.strip()
-
diff --git a/setup.cfg b/setup.cfg
index 688b4fa..b6ab484 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -29,6 +29,16 @@ include_package_data = True
[options.packages.find]
+[mypy]
+ignore_missing_imports = True
+disallow_untyped_defs = True
+
+[mypy-mu4web.main]
+# NOTE Flask endpoints aren't typed.
+# Prefer to move everything except flask endpoints
+# to proper modules
+disallow_untyped_defs = False
+
[flake8]
ignore = E731
max-line-length = 100