From 322154041ad6ac48d26e409512445993c59fd452 Mon Sep 17 00:00:00 2001 From: Bastian Venthur Date: Wed, 31 Aug 2022 17:17:02 +0200 Subject: [PATCH] WIP --- Makefile | 4 +++ blag/blag.py | 83 ++++++++++++++++++++++++++++---------------- blag/devserver.py | 13 +++---- blag/markdown.py | 17 +++++---- blag/quickstart.py | 5 +-- requirements-dev.txt | 2 ++ setup.cfg | 6 ++++ 7 files changed, 87 insertions(+), 43 deletions(-) diff --git a/Makefile b/Makefile index d973043..3199902 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,10 @@ $(VENV): requirements.txt requirements-dev.txt setup.py test: $(VENV) $(BIN)/pytest +.PHONY: mypy +mypy: $(VENV) + $(BIN)/mypy + .PHONY: lint lint: $(VENV) $(BIN)/flake8 diff --git a/blag/blag.py b/blag/blag.py index e84bc2b..bf70e3d 100644 --- a/blag/blag.py +++ b/blag/blag.py @@ -11,7 +11,13 @@ import logging import configparser import sys -from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PackageLoader +from jinja2 import ( + Environment, + ChoiceLoader, + FileSystemLoader, + PackageLoader, + Template, +) import feedgenerator from blag.markdown import markdown_factory, convert_markdown @@ -26,7 +32,7 @@ logging.basicConfig( ) -def main(args=None): +def main(arguments: list[str] = None) -> None: """Main entrypoint for the CLI. This method parses the CLI arguments and executes the respective @@ -34,11 +40,11 @@ def main(args=None): Parameters ---------- - args : list[str] + arguments : list[str] optional parameters, used for testing """ - args = parse_args(args) + args = parse_args(arguments) # set loglevel if args.verbose: logger.setLevel(logging.DEBUG) @@ -46,7 +52,7 @@ def main(args=None): args.func(args) -def parse_args(args=None): +def parse_args(args: list[str] = None) -> argparse.Namespace: """Parse command line arguments. Parameters @@ -135,7 +141,7 @@ def parse_args(args=None): return parser.parse_args(args) -def get_config(configfile): +def get_config(configfile: str) -> configparser.SectionProxy: """Load site configuration from configfile. Parameters @@ -146,7 +152,7 @@ def get_config(configfile): Returns ------- - dict + configparser.SectionProxy """ config = configparser.ConfigParser() @@ -166,7 +172,10 @@ def get_config(configfile): return config['main'] -def environment_factory(template_dir=None, globals_=None): +def environment_factory( + template_dir: str = None, + globals_: dict = None, +) -> Environment: """Environment factory. Creates a Jinja2 Environment with the default templates and @@ -186,7 +195,7 @@ def environment_factory(template_dir=None, globals_=None): """ # first we try the custom templates, and fall back the ones provided # by blag - loaders = [] + loaders: list[FileSystemLoader | PackageLoader] = [] if template_dir: loaders.append(FileSystemLoader([template_dir])) loaders.append(PackageLoader('blag', 'templates')) @@ -196,7 +205,7 @@ def environment_factory(template_dir=None, globals_=None): return env -def build(args): +def build(args: argparse.Namespace) -> None: """Build the site. This is blag's main method that builds the site, generates the feed @@ -261,8 +270,13 @@ def build(args): generate_tags(articles, tags_template, tag_template, args.output_dir) -def process_markdown(convertibles, input_dir, output_dir, - page_template, article_template): +def process_markdown( + convertibles: list[tuple[str, str]], + input_dir: str, + output_dir: str, + page_template: Template, + article_template: Template, +) -> tuple[list[tuple[str, dict]], list[tuple[str, dict]]]: """Process markdown files. This method processes the convertibles, converts them to html and @@ -273,7 +287,7 @@ def process_markdown(convertibles, input_dir, output_dir, Parameters ---------- - convertibles : List[Tuple[str, str]] + convertibles : list[tuple[str, str]] relative paths to markdown- (src) html- (dest) files input_dir : str output_dir : str @@ -282,7 +296,7 @@ def process_markdown(convertibles, input_dir, output_dir, Returns ------- - articles, pages : List[Tuple[str, Dict]] + articles, pages : list[tuple[str, dict]] """ logger.info("Converting Markdown files...") @@ -318,18 +332,18 @@ def process_markdown(convertibles, input_dir, output_dir, def generate_feed( - articles, - output_dir, - base_url, - blog_title, - blog_description, - blog_author, -): + articles: list[tuple[str, dict]], + output_dir: str, + base_url: str, + blog_title: str, + blog_description: str, + blog_author: str, +) -> None: """Generate Atom feed. Parameters ---------- - articles : list[list[str, dict]] + articles : list[tuple[str, dict]] list of relative output path and article dictionary output_dir : str where the feed is stored @@ -369,12 +383,16 @@ def generate_feed( feed.write(fh, encoding='utf8') -def generate_archive(articles, template, output_dir): +def generate_archive( + articles: list[tuple[str, dict]], + template: Template, + output_dir: str, +) -> None: """Generate the archive page. Parameters ---------- - articles : list[list[str, dict]] + articles : list[tuple[str, dict]] List of articles. Each article has the destination path and a dictionary with the content. template : jinja2.Template instance @@ -392,12 +410,17 @@ def generate_archive(articles, template, output_dir): fh.write(result) -def generate_tags(articles, tags_template, tag_template, output_dir): +def generate_tags( + articles: list[tuple[str, dict]], + tags_template: Template, + tag_template: Template, + output_dir: str, +) -> None: """Generate the tags page. Parameters ---------- - articles : list[list[str, dict]] + articles : list[tuple[str, dict]] List of articles. Each article has the destination path and a dictionary with the content. tags_template, tag_template : jinja2.Template instance @@ -408,15 +431,17 @@ def generate_tags(articles, tags_template, tag_template, output_dir): os.makedirs(f'{output_dir}/tags', exist_ok=True) # get tags number of occurrences - all_tags = {} + all_tags: dict = {} for _, context in articles: tags = context.get('tags', []) for tag in tags: all_tags[tag] = all_tags.get(tag, 0) + 1 # sort by occurrence - all_tags = sorted(all_tags.items(), key=lambda x: x[1], reverse=True) + taglist: list[tuple[str, int]] = sorted( + all_tags.items(), key=lambda x: x[1], reverse=True + ) - result = tags_template.render(dict(tags=all_tags)) + result = tags_template.render(dict(tags=taglist)) with open(f'{output_dir}/tags/index.html', 'w') as fh: fh.write(result) diff --git a/blag/devserver.py b/blag/devserver.py index dbc7386..c49cdfe 100644 --- a/blag/devserver.py +++ b/blag/devserver.py @@ -12,6 +12,7 @@ import time import multiprocessing from http.server import SimpleHTTPRequestHandler, HTTPServer from functools import partial +import argparse from blag import blag @@ -19,7 +20,7 @@ from blag import blag logger = logging.getLogger(__name__) -def get_last_modified(dirs): +def get_last_modified(dirs: list[str]) -> float: """Get the last modified time. This method recursively goes through `dirs` and returns the most @@ -32,11 +33,11 @@ def get_last_modified(dirs): Returns ------- - int + float most recent modification time found in `dirs` """ - last_mtime = 0 + last_mtime = 0.0 for dir in dirs: for root, dirs, files in os.walk(dir): @@ -48,7 +49,7 @@ def get_last_modified(dirs): return last_mtime -def autoreload(args): +def autoreload(args: argparse.Namespace) -> None: """Start the autoreloader. This method monitors the given directories for changes (i.e. the @@ -67,7 +68,7 @@ def autoreload(args): logger.info(f'Monitoring {dirs} for changes...') # make sure we trigger the rebuild immediately when we enter the # loop to avoid serving stale contents - last_mtime = 0 + last_mtime = 0.0 while True: mtime = get_last_modified(dirs) if mtime > last_mtime: @@ -77,7 +78,7 @@ def autoreload(args): time.sleep(1) -def serve(args): +def serve(args: argparse.Namespace) -> None: """Start the webserver and the autoreloader. Parameters diff --git a/blag/markdown.py b/blag/markdown.py index 328b0d0..640bb3b 100644 --- a/blag/markdown.py +++ b/blag/markdown.py @@ -8,6 +8,7 @@ processing. from datetime import datetime import logging from urllib.parse import urlsplit, urlunsplit +from xml.etree.ElementTree import Element from markdown import Markdown from markdown.extensions import Extension @@ -17,7 +18,7 @@ from markdown.treeprocessors import Treeprocessor logger = logging.getLogger(__name__) -def markdown_factory(): +def markdown_factory() -> Markdown: """Create a Markdown instance. This method exists only to ensure we use the same Markdown instance @@ -33,12 +34,12 @@ def markdown_factory(): 'meta', 'fenced_code', 'codehilite', 'smarty', MarkdownLinkExtension() ], - output_format='html5', + output_format='html', ) return md -def convert_markdown(md, markdown): +def convert_markdown(md: Markdown, markdown: str) -> tuple[str, dict]: """Convert markdown into html and extract meta data. Some meta data is treated special: @@ -87,15 +88,19 @@ class MarkdownLinkTreeprocessor(Treeprocessor): """ - def run(self, root): + def run(self, root: Element): for element in root.iter(): if element.tag == 'a': url = element.get('href') + # element.get could also return None, we haven't seen this so + # far, so lets wait if we raise this + assert url is not None + url = str(url) converted = self.convert(url) element.set('href', converted) return root - def convert(self, url): + def convert(self, url: str): scheme, netloc, path, query, fragment = urlsplit(url) logger.debug( f'{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}' @@ -113,7 +118,7 @@ class MarkdownLinkExtension(Extension): """markdown.extension that converts relative .md- to .html-links. """ - def extendMarkdown(self, md): + def extendMarkdown(self, md: Markdown): md.treeprocessors.register( MarkdownLinkTreeprocessor(md), 'mdlink', 0, ) diff --git a/blag/quickstart.py b/blag/quickstart.py index 00b4725..7bffc2c 100644 --- a/blag/quickstart.py +++ b/blag/quickstart.py @@ -3,9 +3,10 @@ """ import configparser +import argparse -def get_input(question, default): +def get_input(question: str, default: str) -> str: """Prompt for user input. This is a wrapper around the input-builtin. It will show the default answer @@ -29,7 +30,7 @@ def get_input(question, default): return reply -def quickstart(args): +def quickstart(args: argparse.Namespace) -> None: """Quickstart. This method asks the user some questions and generates a diff --git a/requirements-dev.txt b/requirements-dev.txt index 1292db9..fd43f91 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,3 +4,5 @@ wheel==0.37.1 pytest==7.1.2 pytest-cov==3.0.0 flake8==5.0.2 +mypy==0.971 +types-markdown==3.4.1 diff --git a/setup.cfg b/setup.cfg index 681fad8..71030af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,9 @@ addopts = [flake8] exclude = venv,build,docs + +[mypy] +files = blag,tests + +[mypy-feedgenerator.*] +ignore_missing_imports = True