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