From d17967eea2e42466103347257002451f17b5d5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Tue, 2 May 2023 03:00:59 +0200 Subject: 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`. --- Makefile | 6 ++++- mu4web/maildir.py | 7 +++-- mu4web/main.py | 8 +++--- mu4web/mu.py | 70 +++++++++++++++++++++++++++++++++---------------- mu4web/password.py | 2 +- mu4web/user/__init__.py | 8 +++--- mu4web/util.py | 9 +++++-- mu4web/xapian.py | 6 ++--- setup.cfg | 10 +++++++ 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" 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 -- cgit v1.2.3