diff --git a/blag/blag.py b/blag/blag.py index c59ae0a..28fe983 100644 --- a/blag/blag.py +++ b/blag/blag.py @@ -6,32 +6,28 @@ # remove when we don't support py38 anymore from __future__ import annotations -from typing import Any + import argparse +import configparser +import logging import os import shutil -import logging -import configparser import sys +from typing import Any -from jinja2 import ( - Environment, - FileSystemLoader, - Template, - TemplateNotFound, -) import feedgenerator +from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound import blag -from blag.markdown import markdown_factory, convert_markdown from blag.devserver import serve -from blag.version import __VERSION__ +from blag.markdown import convert_markdown, markdown_factory from blag.quickstart import quickstart +from blag.version import __VERSION__ logger = logging.getLogger(__name__) logging.basicConfig( level=logging.INFO, - format='%(asctime)s %(levelname)s %(name)s %(message)s', + format="%(asctime)s %(levelname)s %(name)s %(message)s", ) @@ -70,84 +66,84 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace: """ parser = argparse.ArgumentParser() parser.add_argument( - '--version', - action='version', - version='%(prog)s ' + __VERSION__, + "--version", + action="version", + version="%(prog)s " + __VERSION__, ) parser.add_argument( - '-v', - '--verbose', - action='store_true', - help='Verbose output.', + "-v", + "--verbose", + action="store_true", + help="Verbose output.", ) - commands = parser.add_subparsers(dest='command') + commands = parser.add_subparsers(dest="command") 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', + "quickstart", help="Quickstart blag, creating necessary configuration.", ) quickstart_parser.set_defaults(func=quickstart) serve_parser = commands.add_parser( - 'serve', + "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) @@ -170,18 +166,18 @@ def get_config(configfile: str) -> configparser.SectionProxy: config = configparser.ConfigParser() config.read(configfile) # check for the mandatory options - for value in 'base_url', 'title', 'description', 'author': + for value in "base_url", "title", "description", "author": try: - config['main'][value] + config["main"][value] except Exception: - print(f'{value} is missing in {configfile}!') + print(f"{value} is missing in {configfile}!") sys.exit(1) - if not config['main']['base_url'].endswith('/'): - logger.warning('base_url does not end with a slash, adding it.') - config['main']['base_url'] += '/' + if not config["main"]["base_url"].endswith("/"): + logger.warning("base_url does not end with a slash, adding it.") + config["main"]["base_url"] += "/" - return config['main'] + return config["main"] def environment_factory( @@ -222,51 +218,51 @@ def build(args: argparse.Namespace) -> None: args """ - os.makedirs(f'{args.output_dir}', exist_ok=True) + 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 + 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'): + if rel_src.endswith(".md"): rel_dst = rel_src - rel_dst = rel_dst[:-3] + '.html' + 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}', + 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) - os.makedirs(f'{args.output_dir}/{path}', exist_ok=True) + path = os.path.relpath(f"{root}/{dirname}", start=args.input_dir) + os.makedirs(f"{args.output_dir}/{path}", exist_ok=True) # copy static files over - logger.info('Copying static files.') + logger.info("Copying static files.") if os.path.exists(args.static_dir): shutil.copytree(args.static_dir, args.output_dir, dirs_exist_ok=True) - config = get_config('config.ini') + config = get_config("config.ini") env = environment_factory(args.template_dir, dict(site=config)) try: - page_template = env.get_template('page.html') - article_template = env.get_template('article.html') - index_template = env.get_template('index.html') - archive_template = env.get_template('archive.html') - tags_template = env.get_template('tags.html') - tag_template = env.get_template('tag.html') + page_template = env.get_template("page.html") + article_template = env.get_template("article.html") + index_template = env.get_template("index.html") + archive_template = env.get_template("archive.html") + tags_template = env.get_template("tags.html") + tag_template = env.get_template("tag.html") except TemplateNotFound as exc: - tmpl = os.path.join(blag.__path__[0], 'templates') + tmpl = os.path.join(blag.__path__[0], "templates") logger.error( f'Template "{exc.name}" not found in {args.template_dir}! ' - 'Consider running `blag quickstart` or copying the ' - f'missing template from {tmpl}.' + "Consider running `blag quickstart` or copying the " + f"missing template from {tmpl}." ) sys.exit(1) @@ -282,10 +278,10 @@ def build(args: argparse.Namespace) -> None: generate_feed( articles, args.output_dir, - base_url=config['base_url'], - blog_title=config['title'], - blog_description=config['description'], - blog_author=config['author'], + base_url=config["base_url"], + blog_title=config["title"], + blog_description=config["description"], + blog_author=config["author"], ) generate_index(articles, index_template, args.output_dir) generate_archive(articles, archive_template, args.output_dir) @@ -330,9 +326,9 @@ def process_markdown( articles = [] pages = [] for src, dst in convertibles: - logger.debug(f'Processing {src}') + logger.debug(f"Processing {src}") - with open(f'{input_dir}/{src}', 'r') as fh: + with open(f"{input_dir}/{src}", "r") as fh: body = fh.read() content, meta = convert_markdown(md, body) @@ -342,17 +338,17 @@ def process_markdown( # if markdown has date in meta, we treat it as a blog article, # everything else are just pages - if meta and 'date' in meta: + if meta and "date" in meta: articles.append((dst, context)) result = article_template.render(context) else: pages.append((dst, context)) result = page_template.render(context) - with open(f'{output_dir}/{dst}', 'w') as fh_dest: + with open(f"{output_dir}/{dst}", "w") as fh_dest: fh_dest.write(result) # sort articles by date, descending - articles = sorted(articles, key=lambda x: x[1]['date'], reverse=True) + articles = sorted(articles, key=lambda x: x[1]["date"], reverse=True) return articles, pages @@ -382,30 +378,30 @@ def generate_feed( blog author """ - logger.info('Generating Atom feed.') + logger.info("Generating Atom feed.") feed = feedgenerator.Atom1Feed( link=base_url, title=blog_title, description=blog_description, - feed_url=base_url + 'atom.xml', + feed_url=base_url + "atom.xml", ) for dst, context in articles: # if article has a description, use that. otherwise fall back to # the title - description = context.get('description', context['title']) + description = context.get("description", context["title"]) feed.add_item( - title=context['title'], + title=context["title"], author_name=blog_author, link=base_url + dst, description=description, - content=context['content'], - pubdate=context['date'], + content=context["content"], + pubdate=context["date"], ) - with open(f'{output_dir}/atom.xml', 'w') as fh: - feed.write(fh, encoding='utf8') + with open(f"{output_dir}/atom.xml", "w") as fh: + feed.write(fh, encoding="utf8") def generate_index( @@ -429,11 +425,11 @@ def generate_index( archive = [] for dst, context in articles: entry = context.copy() - entry['dst'] = dst + entry["dst"] = dst archive.append(entry) result = template.render(dict(archive=archive)) - with open(f'{output_dir}/index.html', 'w') as fh: + with open(f"{output_dir}/index.html", "w") as fh: fh.write(result) @@ -458,11 +454,11 @@ def generate_archive( archive = [] for dst, context in articles: entry = context.copy() - entry['dst'] = dst + entry["dst"] = dst archive.append(entry) result = template.render(dict(archive=archive)) - with open(f'{output_dir}/archive.html', 'w') as fh: + with open(f"{output_dir}/archive.html", "w") as fh: fh.write(result) @@ -484,11 +480,11 @@ def generate_tags( """ 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 all_tags: dict[str, int] = {} for _, context in articles: - tags: list[str] = 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 @@ -497,25 +493,25 @@ def generate_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) # get tags and archive per tag all_tags2: dict[str, list[dict[str, Any]]] = {} for dst, context in articles: - tags = context.get('tags', []) + tags = context.get("tags", []) for tag in tags: archive: list[dict[str, Any]] = all_tags2.get(tag, []) entry = context.copy() - entry['dst'] = dst + entry["dst"] = dst archive.append(entry) all_tags2[tag] = archive 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: + with open(f"{output_dir}/tags/{tag}.html", "w") as fh: fh.write(result) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/blag/devserver.py b/blag/devserver.py index a57ad71..4f3ec50 100644 --- a/blag/devserver.py +++ b/blag/devserver.py @@ -8,18 +8,18 @@ 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 +import logging +import multiprocessing +import os +import time +from functools import partial +from http.server import HTTPServer, SimpleHTTPRequestHandler +from typing import NoReturn from blag import blag - logger = logging.getLogger(__name__) @@ -69,7 +69,7 @@ def autoreload(args: argparse.Namespace) -> NoReturn: """ 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 # loop to avoid serving stale contents last_mtime = 0.0 @@ -77,7 +77,7 @@ def autoreload(args: argparse.Namespace) -> NoReturn: mtime = get_last_modified(dirs) if mtime > last_mtime: last_mtime = mtime - logger.info('Change detected, rebuilding...') + logger.info("Change detected, rebuilding...") blag.build(args) time.sleep(1) @@ -92,7 +92,7 @@ def serve(args: argparse.Namespace) -> None: """ httpd = HTTPServer( - ('', 8000), + ("", 8000), partial(SimpleHTTPRequestHandler, directory=args.output_dir), ) proc = multiprocessing.Process(target=autoreload, args=(args,)) diff --git a/blag/markdown.py b/blag/markdown.py index 6b055ed..e23d1e9 100644 --- a/blag/markdown.py +++ b/blag/markdown.py @@ -7,8 +7,9 @@ processing. # remove when we don't support py38 anymore from __future__ import annotations -from datetime import datetime + import logging +from datetime import datetime from urllib.parse import urlsplit, urlunsplit from xml.etree.ElementTree import Element @@ -16,7 +17,6 @@ from markdown import Markdown from markdown.extensions import Extension from markdown.treeprocessors import Treeprocessor - logger = logging.getLogger(__name__) @@ -33,13 +33,13 @@ def markdown_factory() -> Markdown: """ md = Markdown( extensions=[ - 'meta', - 'fenced_code', - 'codehilite', - 'smarty', + "meta", + "fenced_code", + "codehilite", + "smarty", MarkdownLinkExtension(), ], - output_format='html', + output_format="html", ) return md @@ -75,20 +75,20 @@ def convert_markdown( # markdowns metadata consists as list of strings -- one item per # line. let's convert into single strings. for key, value in meta.items(): - value = '\n'.join(value) + value = "\n".join(value) meta[key] = value # convert known metadata # date: datetime - if 'date' in meta: - meta['date'] = datetime.fromisoformat(meta['date']) - meta['date'] = meta['date'].astimezone() + if "date" in meta: + meta["date"] = datetime.fromisoformat(meta["date"]) + meta["date"] = meta["date"].astimezone() # tags: list[str] and lower case - if 'tags' in meta: - tags = meta['tags'].split(',') + if "tags" in meta: + tags = meta["tags"].split(",") tags = [t.lower() for t in tags] tags = [t.strip() for t in tags] - meta['tags'] = tags + meta["tags"] = tags return content, meta @@ -98,25 +98,25 @@ class MarkdownLinkTreeprocessor(Treeprocessor): def run(self, root: Element) -> Element: for element in root.iter(): - if element.tag == 'a': - url = element.get('href') + 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) + element.set("href", converted) return root def convert(self, url: str) -> str: scheme, netloc, path, query, fragment = urlsplit(url) logger.debug( - f'{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}' + f"{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}" ) if scheme or netloc or not path: return url - if path.endswith('.md'): - path = path[:-3] + '.html' + if path.endswith(".md"): + path = path[:-3] + ".html" url = urlunsplit((scheme, netloc, path, query, fragment)) return url @@ -128,6 +128,6 @@ class MarkdownLinkExtension(Extension): def extendMarkdown(self, md: Markdown) -> None: md.treeprocessors.register( MarkdownLinkTreeprocessor(md), - 'mdlink', + "mdlink", 0, ) diff --git a/blag/quickstart.py b/blag/quickstart.py index ac40112..42e8425 100644 --- a/blag/quickstart.py +++ b/blag/quickstart.py @@ -4,10 +4,11 @@ # remove when we don't support py38 anymore from __future__ import annotations -import configparser + import argparse -import shutil +import configparser import os +import shutil import blag @@ -47,7 +48,7 @@ def copy_default_theme() -> None: """ print("Copying default theme...") - for dir_ in 'templates', 'content', 'static': + for dir_ in "templates", "content", "static": print(f" Copying {dir_}...") try: shutil.copytree( @@ -89,13 +90,13 @@ def quickstart(args: argparse.Namespace | None) -> None: ) config = configparser.ConfigParser() - config['main'] = { - 'base_url': base_url, - 'title': title, - 'description': description, - 'author': author, + config["main"] = { + "base_url": base_url, + "title": title, + "description": description, + "author": author, } - with open('config.ini', 'w') as fh: + with open("config.ini", "w") as fh: config.write(fh) copy_default_theme() diff --git a/blag/version.py b/blag/version.py index 3fe4bea..7e6a9cc 100644 --- a/blag/version.py +++ b/blag/version.py @@ -1 +1 @@ -__VERSION__ = '1.5.0' +__VERSION__ = "1.5.0" diff --git a/tests/conftest.py b/tests/conftest.py index 81215ec..f852463 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ # 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 +from argparse import Namespace +from tempfile import TemporaryDirectory +from typing import Callable, Iterator import pytest from jinja2 import Environment, Template @@ -14,43 +15,43 @@ from blag import blag, quickstart @pytest.fixture def environment(cleandir: str) -> Iterator[Environment]: site = { - 'base_url': 'site base_url', - 'title': 'site title', - 'description': 'site description', - 'author': 'site author', + "base_url": "site base_url", + "title": "site title", + "description": "site description", + "author": "site author", } - env = blag.environment_factory('templates', globals_=dict(site=site)) + env = blag.environment_factory("templates", globals_=dict(site=site)) yield env @pytest.fixture def page_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('page.html') + yield environment.get_template("page.html") @pytest.fixture def article_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('article.html') + yield environment.get_template("article.html") @pytest.fixture def index_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('index.html') + yield environment.get_template("index.html") @pytest.fixture def archive_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('archive.html') + yield environment.get_template("archive.html") @pytest.fixture def tags_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('tags.html') + yield environment.get_template("tags.html") @pytest.fixture def tag_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('tag.html') + yield environment.get_template("tag.html") @pytest.fixture @@ -65,8 +66,8 @@ author = a. u. thor """ with TemporaryDirectory() as dir: - os.mkdir(f'{dir}/build') - with open(f'{dir}/config.ini', 'w') as fh: + os.mkdir(f"{dir}/build") + with open(f"{dir}/config.ini", "w") as fh: fh.write(config) # change directory old_cwd = os.getcwd() @@ -79,11 +80,10 @@ author = a. u. thor @pytest.fixture def args(cleandir: Callable[[], Iterator[str]]) -> Iterator[Namespace]: - args = Namespace( - input_dir='content', - output_dir='build', - static_dir='static', - template_dir='templates', + 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 ce2dd4a..e454ca1 100644 --- a/tests/test_blag.py +++ b/tests/test_blag.py @@ -1,73 +1,73 @@ # 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 +from datetime import datetime +from tempfile import TemporaryDirectory +from typing import Any import pytest -from pytest import CaptureFixture, LogCaptureFixture from jinja2 import Template +from pytest import CaptureFixture, LogCaptureFixture -from blag import __VERSION__ -from blag import blag +from blag import __VERSION__, blag 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') + blag.generate_feed(articles, "build", " ", " ", " ", " ") + assert os.path.exists("build/atom.xml") def test_feed(cleandir: str) -> None: articles: list[tuple[str, dict[str, Any]]] = [ ( - 'dest1.html', + "dest1.html", { - 'title': 'title1', - 'date': datetime(2019, 6, 6), - 'content': 'content1', + "title": "title1", + "date": datetime(2019, 6, 6), + "content": "content1", }, ), ( - 'dest2.html', + "dest2.html", { - 'title': 'title2', - 'date': datetime(1980, 5, 9), - 'content': 'content2', + "title": "title2", + "date": datetime(1980, 5, 9), + "content": "content2", }, ), ] blag.generate_feed( articles, - 'build', - 'https://example.com/', - 'blog title', - 'blog description', - 'blog author', + "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() - assert '