diff --git a/.github/workflows/python-package.yaml b/.github/workflows/python-package.yaml
index fdccf0d..cb0b346 100644
--- a/.github/workflows/python-package.yaml
+++ b/.github/workflows/python-package.yaml
@@ -36,3 +36,7 @@ jobs:
- name: Run linter
run: |
make lint
+
+ - name: Run mypy
+ run: |
+ make mypy
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6eef1dd..b7b4dcf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
## unreleased
+* added type hints and mypy --strict to test suite
* improved default template
* updated dependencies:
* markdown 3.4.1
diff --git a/Makefile b/Makefile
index d973043..87cec44 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@ endif
.PHONY: all
-all: lint test
+all: lint mypy test
$(VENV): requirements.txt requirements-dev.txt setup.py
$(PY) -m venv $(VENV)
@@ -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/__init__.py b/blag/__init__.py
index 6527216..189ce64 100644
--- a/blag/__init__.py
+++ b/blag/__init__.py
@@ -1 +1 @@
-from blag.version import __VERSION__ # noqa
+from blag.version import __VERSION__ as __VERSION__ # noqa
diff --git a/blag/blag.py b/blag/blag.py
index e84bc2b..490348b 100644
--- a/blag/blag.py
+++ b/blag/blag.py
@@ -4,6 +4,9 @@
"""
+# remove when we don't support py38 anymore
+from __future__ import annotations
+from typing import Any
import argparse
import os
import shutil
@@ -11,7 +14,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
@@ -21,12 +30,12 @@ from blag.quickstart import quickstart
logger = logging.getLogger(__name__)
logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s %(levelname)s %(name)s %(message)s',
+ level=logging.INFO,
+ format='%(asctime)s %(levelname)s %(name)s %(message)s',
)
-def main(args=None):
+def main(arguments: list[str] | None = None) -> None:
"""Main entrypoint for the CLI.
This method parses the CLI arguments and executes the respective
@@ -34,11 +43,11 @@ def main(args=None):
Parameters
----------
- args : list[str]
+ arguments
optional parameters, used for testing
"""
- args = parse_args(args)
+ args = parse_args(arguments)
# set loglevel
if args.verbose:
logger.setLevel(logging.DEBUG)
@@ -46,12 +55,12 @@ def main(args=None):
args.func(args)
-def parse_args(args=None):
+def parse_args(args: list[str] | None = None) -> argparse.Namespace:
"""Parse command line arguments.
Parameters
----------
- args : List[str]
+ args
optional parameters, used for testing
Returns
@@ -63,10 +72,11 @@ def parse_args(args=None):
parser.add_argument(
'--version',
action='version',
- version='%(prog)s '+__VERSION__,
+ version='%(prog)s ' + __VERSION__,
)
parser.add_argument(
- '-v', '--verbose',
+ '-v',
+ '--verbose',
action='store_true',
help='Verbose output.',
)
@@ -75,78 +85,86 @@ def parse_args(args=None):
commands.required = True
build_parser = commands.add_parser(
- 'build',
- help='Build website.',
+ 'build',
+ help='Build website.',
)
build_parser.set_defaults(func=build)
build_parser.add_argument(
- '-i', '--input-dir',
- default='content',
- help='Input directory (default: content)',
+ '-i',
+ '--input-dir',
+ default='content',
+ help='Input directory (default: content)',
)
build_parser.add_argument(
- '-o', '--output-dir',
- default='build',
- help='Ouptut directory (default: build)',
+ '-o',
+ '--output-dir',
+ default='build',
+ help='Ouptut directory (default: build)',
)
build_parser.add_argument(
- '-t', '--template-dir',
- default='templates',
- help='Template directory (default: templates)',
+ '-t',
+ '--template-dir',
+ default='templates',
+ help='Template directory (default: templates)',
)
build_parser.add_argument(
- '-s', '--static-dir',
- default='static',
- help='Static directory (default: static)',
+ '-s',
+ '--static-dir',
+ default='static',
+ help='Static directory (default: static)',
)
quickstart_parser = commands.add_parser(
- 'quickstart',
- help="Quickstart blag, creating necessary configuration.",
+ 'quickstart',
+ help="Quickstart blag, creating necessary configuration.",
)
quickstart_parser.set_defaults(func=quickstart)
serve_parser = commands.add_parser(
- 'serve',
- help="Start development server.",
+ 'serve',
+ help="Start development server.",
)
serve_parser.set_defaults(func=serve)
serve_parser.add_argument(
- '-i', '--input-dir',
- default='content',
- help='Input directory (default: content)',
+ '-i',
+ '--input-dir',
+ default='content',
+ help='Input directory (default: content)',
)
serve_parser.add_argument(
- '-o', '--output-dir',
- default='build',
- help='Ouptut directory (default: build)',
+ '-o',
+ '--output-dir',
+ default='build',
+ help='Ouptut directory (default: build)',
)
serve_parser.add_argument(
- '-t', '--template-dir',
- default='templates',
- help='Template directory (default: templates)',
+ '-t',
+ '--template-dir',
+ default='templates',
+ help='Template directory (default: templates)',
)
serve_parser.add_argument(
- '-s', '--static-dir',
- default='static',
- help='Static directory (default: static)',
+ '-s',
+ '--static-dir',
+ default='static',
+ help='Static directory (default: static)',
)
return parser.parse_args(args)
-def get_config(configfile):
+def get_config(configfile: str) -> configparser.SectionProxy:
"""Load site configuration from configfile.
Parameters
----------
- configfile : str
+ configfile
path to configuration file
Returns
-------
- dict
+ configparser.SectionProxy
"""
config = configparser.ConfigParser()
@@ -166,7 +184,10 @@ def get_config(configfile):
return config['main']
-def environment_factory(template_dir=None, globals_=None):
+def environment_factory(
+ template_dir: str | None = None,
+ globals_: dict[str, object] | None = None,
+) -> Environment:
"""Environment factory.
Creates a Jinja2 Environment with the default templates and
@@ -176,8 +197,9 @@ def environment_factory(template_dir=None, globals_=None):
Parameters
----------
- template_dir : str
- globals_ : dict
+ template_dir
+ directory containing the templates
+ globals_
Returns
-------
@@ -186,7 +208,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 +218,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
@@ -204,15 +226,16 @@ def build(args):
Parameters
----------
- args : argparse.Namespace
+ args
"""
os.makedirs(f'{args.output_dir}', exist_ok=True)
convertibles = []
for root, dirnames, filenames in os.walk(args.input_dir):
for filename in filenames:
- rel_src = os.path.relpath(f'{root}/{filename}',
- start=args.input_dir)
+ rel_src = os.path.relpath(
+ f'{root}/{filename}', start=args.input_dir
+ )
# all non-markdown files are just copied over, the markdown
# files are converted to html
if rel_src.endswith('.md'):
@@ -220,8 +243,10 @@ def build(args):
rel_dst = rel_dst[:-3] + '.html'
convertibles.append((rel_src, rel_dst))
else:
- shutil.copy(f'{args.input_dir}/{rel_src}',
- f'{args.output_dir}/{rel_src}')
+ shutil.copy(
+ f'{args.input_dir}/{rel_src}',
+ f'{args.output_dir}/{rel_src}',
+ )
for dirname in dirnames:
# all directories are copied into the output directory
path = os.path.relpath(f'{root}/{dirname}', start=args.input_dir)
@@ -251,7 +276,8 @@ def build(args):
)
generate_feed(
- articles, args.output_dir,
+ articles,
+ args.output_dir,
base_url=config['base_url'],
blog_title=config['title'],
blog_description=config['description'],
@@ -261,8 +287,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[str, Any]]], list[tuple[str, dict[str, Any]]]]:
"""Process markdown files.
This method processes the convertibles, converts them to html and
@@ -273,16 +304,17 @@ def process_markdown(convertibles, input_dir, output_dir,
Parameters
----------
- convertibles : List[Tuple[str, str]]
+ convertibles
relative paths to markdown- (src) html- (dest) files
- input_dir : str
- output_dir : str
- page_template, archive_template : jinja2 template
+ input_dir
+ output_dir
+ page_template, archive_template
templats for pages and articles
Returns
-------
- articles, pages : List[Tuple[str, Dict]]
+ list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]]
+ articles and pages
"""
logger.info("Converting Markdown files...")
@@ -318,37 +350,37 @@ 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[str, Any]]],
+ 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 of relative output path and article dictionary
- output_dir : str
+ output_dir
where the feed is stored
- base_url : str
+ base_url
base url
- blog_title : str
+ blog_title
blog title
- blog_description : str
+ blog_description
blog description
- blog_author : str
+ blog_author
blog author
"""
logger.info('Generating Atom feed.')
feed = feedgenerator.Atom1Feed(
- link=base_url,
- title=blog_title,
- description=blog_description,
- feed_url=base_url + 'atom.xml',
+ link=base_url,
+ title=blog_title,
+ description=blog_description,
+ feed_url=base_url + 'atom.xml',
)
for dst, context in articles:
@@ -369,16 +401,20 @@ def generate_feed(
feed.write(fh, encoding='utf8')
-def generate_archive(articles, template, output_dir):
+def generate_archive(
+ articles: list[tuple[str, dict[str, Any]]],
+ template: Template,
+ output_dir: str,
+) -> None:
"""Generate the archive page.
Parameters
----------
- articles : list[list[str, dict]]
+ articles
List of articles. Each article has the destination path and a
dictionary with the content.
- template : jinja2.Template instance
- output_dir : str
+ template
+ output_dir
"""
archive = []
@@ -392,46 +428,52 @@ 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[str, Any]]],
+ tags_template: Template,
+ tag_template: Template,
+ output_dir: str,
+) -> None:
"""Generate the tags page.
Parameters
----------
- articles : list[list[str, dict]]
+ articles
List of articles. Each article has the destination path and a
dictionary with the content.
- tags_template, tag_template : jinja2.Template instance
- output_dir : str
+ tags_template, tag_template
+ output_dir
"""
logger.info("Generating Tag-pages.")
os.makedirs(f'{output_dir}/tags', exist_ok=True)
-
# get tags number of occurrences
- all_tags = {}
+ all_tags: dict[str, int] = {}
for _, context in articles:
- tags = context.get('tags', [])
+ tags: list[str] = 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)
# get tags and archive per tag
- all_tags = {}
+ all_tags2: dict[str, list[dict[str, Any]]] = {}
for dst, context in articles:
tags = context.get('tags', [])
for tag in tags:
- archive = all_tags.get(tag, [])
+ archive: list[dict[str, Any]] = all_tags2.get(tag, [])
entry = context.copy()
entry['dst'] = dst
archive.append(entry)
- all_tags[tag] = archive
+ all_tags2[tag] = archive
- for tag, archive in all_tags.items():
+ for tag, archive in all_tags2.items():
result = tag_template.render(dict(archive=archive, tag=tag))
with open(f'{output_dir}/tags/{tag}.html', 'w') as fh:
fh.write(result)
diff --git a/blag/devserver.py b/blag/devserver.py
index dbc7386..a57ad71 100644
--- a/blag/devserver.py
+++ b/blag/devserver.py
@@ -6,12 +6,16 @@ site if necessary.
"""
+# remove when we don't support py38 anymore
+from __future__ import annotations
+from typing import NoReturn
import os
import logging
import time
import multiprocessing
from http.server import SimpleHTTPRequestHandler, HTTPServer
from functools import partial
+import argparse
from blag import blag
@@ -19,7 +23,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
@@ -27,16 +31,16 @@ def get_last_modified(dirs):
Parameters
----------
- dirs : list[str]
+ dirs
list of directories to search
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 +52,7 @@ def get_last_modified(dirs):
return last_mtime
-def autoreload(args):
+def autoreload(args: argparse.Namespace) -> NoReturn:
"""Start the autoreloader.
This method monitors the given directories for changes (i.e. the
@@ -60,14 +64,15 @@ def autoreload(args):
Parameters
----------
- args : argparse.Namespace
+ args
+ contains the input-, template- and static dir
"""
dirs = [args.input_dir, args.template_dir, args.static_dir]
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,16 +82,19 @@ def autoreload(args):
time.sleep(1)
-def serve(args):
+def serve(args: argparse.Namespace) -> None:
"""Start the webserver and the autoreloader.
Parameters
----------
- args : arparse.Namespace
+ args
+ contains the input-, template- and static dir
"""
- httpd = HTTPServer(('', 8000), partial(SimpleHTTPRequestHandler,
- directory=args.output_dir))
+ httpd = HTTPServer(
+ ('', 8000),
+ partial(SimpleHTTPRequestHandler, directory=args.output_dir),
+ )
proc = multiprocessing.Process(target=autoreload, args=(args,))
proc.start()
logger.info("\n\n Devserver Started -- visit http://localhost:8000\n")
diff --git a/blag/markdown.py b/blag/markdown.py
index 328b0d0..6b055ed 100644
--- a/blag/markdown.py
+++ b/blag/markdown.py
@@ -5,9 +5,12 @@ processing.
"""
+# remove when we don't support py38 anymore
+from __future__ import annotations
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 +20,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
@@ -30,15 +33,21 @@ def markdown_factory():
"""
md = Markdown(
extensions=[
- 'meta', 'fenced_code', 'codehilite', 'smarty',
- MarkdownLinkExtension()
+ '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[str, str]]:
"""Convert markdown into html and extract meta data.
Some meta data is treated special:
@@ -48,18 +57,20 @@ def convert_markdown(md, markdown):
Parameters
----------
- md : markdown.Markdown instance
- markdown : str
+ md
+ the Markdown instance
+ markdown
+ the markdown text that should be converted
Returns
-------
- str, dict :
+ str, dict[str, str]
html and metadata
"""
md.reset()
content = md.convert(markdown)
- meta = md.Meta
+ meta = md.Meta # type: ignore
# markdowns metadata consists as list of strings -- one item per
# line. let's convert into single strings.
@@ -83,24 +94,26 @@ def convert_markdown(md, markdown):
class MarkdownLinkTreeprocessor(Treeprocessor):
- """Converts relative links to .md files to .html
+ """Converts relative links to .md files to .html"""
- """
-
- def run(self, root):
+ def run(self, root: Element) -> 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) -> str:
scheme, netloc, path, query, fragment = urlsplit(url)
logger.debug(
f'{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}'
)
- if (scheme or netloc or not path):
+ if scheme or netloc or not path:
return url
if path.endswith('.md'):
path = path[:-3] + '.html'
@@ -110,10 +123,11 @@ class MarkdownLinkTreeprocessor(Treeprocessor):
class MarkdownLinkExtension(Extension):
- """markdown.extension that converts relative .md- to .html-links.
+ """markdown.extension that converts relative .md- to .html-links."""
- """
- def extendMarkdown(self, md):
+ def extendMarkdown(self, md: Markdown) -> None:
md.treeprocessors.register(
- MarkdownLinkTreeprocessor(md), 'mdlink', 0,
+ MarkdownLinkTreeprocessor(md),
+ 'mdlink',
+ 0,
)
diff --git a/blag/quickstart.py b/blag/quickstart.py
index 00b4725..c27e473 100644
--- a/blag/quickstart.py
+++ b/blag/quickstart.py
@@ -2,10 +2,13 @@
"""
+# remove when we don't support py38 anymore
+from __future__ import annotations
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
@@ -13,14 +16,15 @@ def get_input(question, default):
Parameters
----------
- question : str
+ question
the question the user is presented
- default : str
+ default
the default value that will be used if no answer was given
Returns
-------
str
+ the answer
"""
reply = input(f"{question} [{default}]: ")
@@ -29,7 +33,7 @@ def get_input(question, default):
return reply
-def quickstart(args):
+def quickstart(args: argparse.Namespace | None) -> None:
"""Quickstart.
This method asks the user some questions and generates a
@@ -37,7 +41,8 @@ def quickstart(args):
Parameters
----------
- args : argparse.Namespace
+ args
+ not used
"""
base_url = get_input(
@@ -59,10 +64,10 @@ def quickstart(args):
config = configparser.ConfigParser()
config['main'] = {
- 'base_url': base_url,
- 'title': title,
- 'description': description,
- 'author': author,
+ 'base_url': base_url,
+ 'title': title,
+ 'description': description,
+ 'author': author,
}
with open('config.ini', 'w') as fh:
config.write(fh)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 7d6b6ab..86eea69 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.4
+mypy==0.971
+types-markdown==3.4.1
diff --git a/setup.cfg b/setup.cfg
index 681fad8..94d778c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -7,3 +7,10 @@ addopts =
[flake8]
exclude = venv,build,docs
+
+[mypy]
+files = blag,tests
+strict = True
+
+[mypy-feedgenerator.*]
+ignore_missing_imports = True
diff --git a/tests/conftest.py b/tests/conftest.py
index 221078c..da40e05 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,13 +1,18 @@
+# remove when we don't support py38 anymore
+from __future__ import annotations
+from argparse import Namespace
+from typing import Iterator, Callable
from tempfile import TemporaryDirectory
import os
import pytest
+from jinja2 import Environment, Template
from blag import blag
@pytest.fixture
-def environment():
+def environment() -> Iterator[Environment]:
site = {
'base_url': 'site base_url',
'title': 'site title',
@@ -19,35 +24,33 @@ def environment():
@pytest.fixture
-def page_template(environment):
+def page_template(environment: Environment) -> Iterator[Template]:
yield environment.get_template('page.html')
@pytest.fixture
-def article_template(environment):
+def article_template(environment: Environment) -> Iterator[Template]:
yield environment.get_template('article.html')
@pytest.fixture
-def archive_template(environment):
+def archive_template(environment: Environment) -> Iterator[Template]:
yield environment.get_template('archive.html')
@pytest.fixture
-def tags_template(environment):
+def tags_template(environment: Environment) -> Iterator[Template]:
yield environment.get_template('tags.html')
@pytest.fixture
-def tag_template(environment):
+def tag_template(environment: Environment) -> Iterator[Template]:
yield environment.get_template('tag.html')
@pytest.fixture
-def cleandir():
- """Create a temporary workind directory and cwd.
-
- """
+def cleandir() -> Iterator[str]:
+ """Create a temporary workind directory and cwd."""
config = """
[main]
base_url = https://example.com/
@@ -70,17 +73,12 @@ author = a. u. thor
@pytest.fixture
-def args(cleandir):
+def args(cleandir: Callable[[], Iterator[str]]) -> Iterator[Namespace]:
- class NameSpace:
- def __init__(self, **kwargs):
- for name in kwargs:
- setattr(self, name, kwargs[name])
-
- args = NameSpace(
- input_dir='content',
- output_dir='build',
- static_dir='static',
- template_dir='templates',
+ args = Namespace(
+ input_dir='content',
+ output_dir='build',
+ static_dir='static',
+ template_dir='templates',
)
yield args
diff --git a/tests/test_blag.py b/tests/test_blag.py
index eeafbae..74e8512 100644
--- a/tests/test_blag.py
+++ b/tests/test_blag.py
@@ -1,41 +1,53 @@
+# remove when we don't support py38 anymore
+from __future__ import annotations
from tempfile import TemporaryDirectory
import os
from datetime import datetime
+from typing import Any
+from argparse import Namespace
import pytest
+from pytest import CaptureFixture, LogCaptureFixture
+from jinja2 import Template
+from blag import __VERSION__
from blag import blag
-def test_generate_feed(cleandir):
- articles = []
+def test_generate_feed(cleandir: str) -> None:
+ articles: list[tuple[str, dict[str, Any]]] = []
blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ')
assert os.path.exists('build/atom.xml')
-def test_feed(cleandir):
- articles = [
- [
+def test_feed(cleandir: str) -> None:
+ articles: list[tuple[str, dict[str, Any]]] = [
+ (
'dest1.html',
{
'title': 'title1',
'date': datetime(2019, 6, 6),
'content': 'content1',
- }
- ],
- [
+ },
+ ),
+ (
'dest2.html',
{
'title': 'title2',
'date': datetime(1980, 5, 9),
'content': 'content2',
- }
- ],
-
+ },
+ ),
]
- blag.generate_feed(articles, 'build', 'https://example.com/',
- 'blog title', 'blog description', 'blog author')
+ blag.generate_feed(
+ articles,
+ 'build',
+ 'https://example.com/',
+ 'blog title',
+ 'blog description',
+ 'blog author',
+ )
with open('build/atom.xml') as fh:
feed = fh.read()
@@ -60,18 +72,20 @@ def test_feed(cleandir):
assert ' None:
# if a description is provided, it will be used as the summary in
# the feed, otherwise we simply use the title of the article
- articles = [[
- 'dest.html',
- {
- 'title': 'title',
- 'description': 'description',
- 'date': datetime(2019, 6, 6),
- 'content': 'content',
- }
- ]]
+ articles: list[tuple[str, dict[str, Any]]] = [
+ (
+ 'dest.html',
+ {
+ 'title': 'title',
+ 'description': 'description',
+ 'date': datetime(2019, 6, 6),
+ 'content': 'content',
+ },
+ )
+ ]
blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ')
with open('build/atom.xml') as fh:
@@ -83,7 +97,7 @@ def test_generate_feed_with_description(cleandir):
assert 'content' in feed
-def test_parse_args_build():
+def test_parse_args_build() -> None:
# test default args
args = blag.parse_args(['build'])
assert args.input_dir == 'content'
@@ -116,7 +130,7 @@ def test_parse_args_build():
assert args.static_dir == 'foo'
-def test_get_config():
+def test_get_config() -> None:
config = """
[main]
base_url = https://example.com/
@@ -138,10 +152,9 @@ author = a. u. thor
# a missing required config causes a sys.exit
for x in 'base_url', 'title', 'description', 'author':
- config2 = '\n'.join([line
- for line
- in config.splitlines()
- if not line.startswith(x)])
+ config2 = '\n'.join(
+ [line for line in config.splitlines() if not line.startswith(x)]
+ )
with TemporaryDirectory() as dir:
configfile = f'{dir}/config.ini'
with open(configfile, 'w') as fh:
@@ -166,17 +179,18 @@ author = a. u. thor
assert config_parsed['base_url'] == 'https://example.com/'
-def test_environment_factory():
- globals_ = {
- 'foo': 'bar',
- 'test': 'me'
- }
+def test_environment_factory() -> None:
+ globals_: dict[str, object] = {'foo': 'bar', 'test': 'me'}
env = blag.environment_factory(globals_=globals_)
assert env.globals['foo'] == 'bar'
assert env.globals['test'] == 'me'
-def test_process_markdown(cleandir, page_template, article_template):
+def test_process_markdown(
+ cleandir: str,
+ page_template: Template,
+ article_template: Template,
+) -> None:
page1 = """\
title: some page
@@ -202,17 +216,12 @@ foo bar
convertibles = []
for i, txt in enumerate((page1, article1, article2)):
- i = str(i)
- with open(f'content/{i}', 'w') as fh:
+ with open(f'content/{str(i)}', 'w') as fh:
fh.write(txt)
- convertibles.append([i, i])
+ convertibles.append((str(i), str(i)))
articles, pages = blag.process_markdown(
- convertibles,
- 'content',
- 'build',
- page_template,
- article_template
+ convertibles, 'content', 'build', page_template, article_template
)
assert isinstance(articles, list)
@@ -230,7 +239,7 @@ foo bar
assert 'content' in context
-def test_build(args):
+def test_build(args: Namespace) -> None:
page1 = """\
title: some page
@@ -259,10 +268,9 @@ foo bar
# write some convertibles
convertibles = []
for i, txt in enumerate((page1, article1, article2)):
- i = str(i)
- with open(f'{args.input_dir}/{i}.md', 'w') as fh:
+ with open(f'{args.input_dir}/{str(i)}.md', 'w') as fh:
fh.write(txt)
- convertibles.append([i, i])
+ convertibles.append((str(i), str(i)))
# some static files
with open(f'{args.static_dir}/test', 'w') as fh:
@@ -291,21 +299,21 @@ foo bar
assert os.path.exists(f'{args.output_dir}/tags/bar.html')
-def test_main(cleandir):
+def test_main(cleandir: str) -> None:
blag.main(['build'])
-def test_cli_version(capsys):
+def test_cli_version(capsys: CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as ex:
blag.main(['--version'])
# normal system exit
assert ex.value.code == 0
# proper version reported
out, _ = capsys.readouterr()
- assert blag.__VERSION__ in out
+ assert __VERSION__ in out
-def test_cli_verbose(cleandir, caplog):
+def test_cli_verbose(cleandir: str, caplog: LogCaptureFixture) -> None:
blag.main(['build'])
assert 'DEBUG' not in caplog.text
diff --git a/tests/test_devserver.py b/tests/test_devserver.py
index 7f56a40..4b66b38 100644
--- a/tests/test_devserver.py
+++ b/tests/test_devserver.py
@@ -1,12 +1,15 @@
+# remove when we don't support py38 anymore
+from __future__ import annotations
import time
import threading
+from argparse import Namespace
import pytest
from blag import devserver
-def test_get_last_modified(cleandir):
+def test_get_last_modified(cleandir: str) -> None:
# take initial time
t1 = devserver.get_last_modified(['content'])
@@ -24,14 +27,16 @@ def test_get_last_modified(cleandir):
assert t2 == t3
-def test_autoreload_builds_immediately(args):
+def test_autoreload_builds_immediately(args: Namespace) -> None:
# create a dummy file that can be build
with open('content/test.md', 'w') as fh:
fh.write('boo')
- t = threading.Thread(target=devserver.autoreload,
- args=(args, ),
- daemon=True,)
+ t = threading.Thread(
+ target=devserver.autoreload,
+ args=(args,),
+ daemon=True,
+ )
t0 = devserver.get_last_modified(['build'])
t.start()
# try for 5 seconds...
@@ -44,11 +49,15 @@ def test_autoreload_builds_immediately(args):
assert t1 > t0
-@pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") # noqa
-def test_autoreload(args):
- t = threading.Thread(target=devserver.autoreload,
- args=(args, ),
- daemon=True,)
+@pytest.mark.filterwarnings(
+ "ignore::pytest.PytestUnhandledThreadExceptionWarning"
+)
+def test_autoreload(args: Namespace) -> None:
+ t = threading.Thread(
+ target=devserver.autoreload,
+ args=(args,),
+ daemon=True,
+ )
t.start()
t0 = devserver.get_last_modified(['build'])
diff --git a/tests/test_markdown.py b/tests/test_markdown.py
index 9103ec1..816f310 100644
--- a/tests/test_markdown.py
+++ b/tests/test_markdown.py
@@ -1,4 +1,7 @@
+# remove when we don't support py38 anymore
+from __future__ import annotations
from datetime import datetime
+from typing import Any
import pytest
import markdown
@@ -6,66 +9,77 @@ import markdown
from blag.markdown import convert_markdown, markdown_factory
-@pytest.mark.parametrize("input_, expected", [
- # inline
- ('[test](test.md)', 'test.html'),
- ('[test](test.md "test")', 'test.html'),
- ('[test](a/test.md)', 'a/test.html'),
- ('[test](a/test.md "test")', 'a/test.html'),
- ('[test](/test.md)', '/test.html'),
- ('[test](/test.md "test")', '/test.html'),
- ('[test](/a/test.md)', '/a/test.html'),
- ('[test](/a/test.md "test")', '/a/test.html'),
- # reference
- ('[test][]\n[test]: test.md ''', 'test.html'),
- ('[test][]\n[test]: test.md "test"', 'test.html'),
- ('[test][]\n[test]: a/test.md', 'a/test.html'),
- ('[test][]\n[test]: a/test.md "test"', 'a/test.html'),
- ('[test][]\n[test]: /test.md', '/test.html'),
- ('[test][]\n[test]: /test.md "test"', '/test.html'),
- ('[test][]\n[test]: /a/test.md', '/a/test.html'),
- ('[test][]\n[test]: /a/test.md "test"', '/a/test.html'),
-])
-def test_convert_markdown_links(input_, expected):
+@pytest.mark.parametrize(
+ "input_, expected",
+ [
+ # inline
+ ('[test](test.md)', 'test.html'),
+ ('[test](test.md "test")', 'test.html'),
+ ('[test](a/test.md)', 'a/test.html'),
+ ('[test](a/test.md "test")', 'a/test.html'),
+ ('[test](/test.md)', '/test.html'),
+ ('[test](/test.md "test")', '/test.html'),
+ ('[test](/a/test.md)', '/a/test.html'),
+ ('[test](/a/test.md "test")', '/a/test.html'),
+ # reference
+ ('[test][]\n[test]: test.md ' '', 'test.html'),
+ ('[test][]\n[test]: test.md "test"', 'test.html'),
+ ('[test][]\n[test]: a/test.md', 'a/test.html'),
+ ('[test][]\n[test]: a/test.md "test"', 'a/test.html'),
+ ('[test][]\n[test]: /test.md', '/test.html'),
+ ('[test][]\n[test]: /test.md "test"', '/test.html'),
+ ('[test][]\n[test]: /a/test.md', '/a/test.html'),
+ ('[test][]\n[test]: /a/test.md "test"', '/a/test.html'),
+ ],
+)
+def test_convert_markdown_links(input_: str, expected: str) -> None:
md = markdown_factory()
html, _ = convert_markdown(md, input_)
assert expected in html
-@pytest.mark.parametrize("input_, expected", [
- # scheme
- ('[test](https://)', 'https://'),
- # netloc
- ('[test](//test.md)', '//test.md'),
- # no path
- ('[test]()', ''),
-])
-def test_dont_convert_normal_links(input_, expected):
+@pytest.mark.parametrize(
+ "input_, expected",
+ [
+ # scheme
+ ('[test](https://)', 'https://'),
+ # netloc
+ ('[test](//test.md)', '//test.md'),
+ # no path
+ ('[test]()', ''),
+ ],
+)
+def test_dont_convert_normal_links(input_: str, expected: str) -> None:
md = markdown_factory()
html, _ = convert_markdown(md, input_)
assert expected in html
-@pytest.mark.parametrize("input_, expected", [
- ('foo: bar', {'foo': 'bar'}),
- ('foo: those are several words', {'foo': 'those are several words'}),
- ('tags: this, is, a, test\n', {'tags': ['this', 'is', 'a', 'test']}),
- ('tags: this, IS, a, test', {'tags': ['this', 'is', 'a', 'test']}),
- ('date: 2020-01-01 12:10', {'date':
- datetime(2020, 1, 1, 12, 10).astimezone()}),
-])
-def test_convert_metadata(input_, expected):
+@pytest.mark.parametrize(
+ "input_, expected",
+ [
+ ('foo: bar', {'foo': 'bar'}),
+ ('foo: those are several words', {'foo': 'those are several words'}),
+ ('tags: this, is, a, test\n', {'tags': ['this', 'is', 'a', 'test']}),
+ ('tags: this, IS, a, test', {'tags': ['this', 'is', 'a', 'test']}),
+ (
+ 'date: 2020-01-01 12:10',
+ {'date': datetime(2020, 1, 1, 12, 10).astimezone()},
+ ),
+ ],
+)
+def test_convert_metadata(input_: str, expected: dict[str, Any]) -> None:
md = markdown_factory()
_, meta = convert_markdown(md, input_)
assert expected == meta
-def test_markdown_factory():
+def test_markdown_factory() -> None:
md = markdown_factory()
assert isinstance(md, markdown.Markdown)
-def test_smarty():
+def test_smarty() -> None:
md = markdown_factory()
md1 = """
@@ -79,7 +93,7 @@ this --- is -- a test ...
assert 'hellip' in html
-def test_smarty_code():
+def test_smarty_code() -> None:
md = markdown_factory()
md1 = """
diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py
index 4594497..ac6ccaf 100644
--- a/tests/test_quickstart.py
+++ b/tests/test_quickstart.py
@@ -1,19 +1,24 @@
+# remove when we don't support py38 anymore
+from __future__ import annotations
+
+from pytest import MonkeyPatch
+
from blag.quickstart import get_input, quickstart
-def test_get_input_default_answer(monkeypatch):
+def test_get_input_default_answer(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setattr('builtins.input', lambda x: '')
answer = get_input("foo", "bar")
assert answer == 'bar'
-def test_get_input(monkeypatch):
+def test_get_input(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setattr('builtins.input', lambda x: 'baz')
answer = get_input("foo", "bar")
assert answer == 'baz'
-def test_quickstart(cleandir, monkeypatch):
+def test_quickstart(cleandir: str, monkeypatch: MonkeyPatch) -> None:
monkeypatch.setattr('builtins.input', lambda x: 'foo')
quickstart(None)
with open('config.ini', 'r') as fh:
diff --git a/tests/test_templates.py b/tests/test_templates.py
index e420891..b25388e 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -1,7 +1,11 @@
+# remove when we don't support py38 anymore
+from __future__ import annotations
import datetime
+from jinja2 import Template
-def test_page(page_template):
+
+def test_page(page_template: Template) -> None:
ctx = {
'content': 'this is the content',
'title': 'this is the title',
@@ -11,7 +15,7 @@ def test_page(page_template):
assert 'this is the title' in result
-def test_article(article_template):
+def test_article(article_template: Template) -> None:
ctx = {
'content': 'this is the content',
'title': 'this is the title',
@@ -23,7 +27,7 @@ def test_article(article_template):
assert '1980-05-09' in result
-def test_archive(archive_template):
+def test_archive(archive_template: Template) -> None:
entry = {
'title': 'this is a title',
'dst': 'https://example.com/link',
@@ -41,7 +45,7 @@ def test_archive(archive_template):
assert 'https://example.com/link' in result
-def test_tags(tags_template):
+def test_tags(tags_template: Template) -> None:
tags = [('foo', 42)]
ctx = {
'tags': tags,
@@ -54,7 +58,7 @@ def test_tags(tags_template):
assert '42' in result
-def test_tag(tag_template):
+def test_tag(tag_template: Template) -> None:
entry = {
'title': 'this is a title',
'dst': 'https://example.com/link',
diff --git a/tests/test_version.py b/tests/test_version.py
index 04f8d9a..b772f4b 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -1,5 +1,8 @@
+# remove when we don't support py38 anymore
+from __future__ import annotations
+
import blag
-def test_version():
+def test_version() -> None:
assert isinstance(blag.__VERSION__, str)