diff --git a/htmd/cli.py b/htmd/cli.py index 70e96f4..53e9206 100644 --- a/htmd/cli.py +++ b/htmd/cli.py @@ -4,6 +4,8 @@ import sys import click +from flask import Flask +from flask_flatpages import FlatPages from .utils import ( combine_and_minify_css, @@ -16,7 +18,7 @@ @click.group() @click.version_option() -def cli(): +def cli() -> None: pass # pragma: no cover @@ -27,7 +29,7 @@ def cli(): default=False, help='Include all templates.', ) -def start(all_templates): +def start(all_templates: bool) -> None: # noqa: FBT001 dir_templates = create_directory('templates/') if all_templates: copy_missing_templates() @@ -49,7 +51,7 @@ def start(all_templates): @cli.command('verify', short_help='Verify posts formatting is correct.') -def verify(): +def verify() -> None: # import is here to avoid looking for the config # which doesn't exist until you run start from . import site @@ -91,7 +93,12 @@ def verify(): sys.exit(1) -def set_post_time(app, post, field, date_time): +def set_post_time( + app: Flask, + post: FlatPages, + field: str, + date_time: datetime.datetime, +) -> None: file_path = ( Path(app.config['FLATPAGES_ROOT']) / (post.path + app.config['FLATPAGES_EXTENSION']) @@ -113,7 +120,7 @@ def set_post_time(app, post, field, date_time): file.write(line) -def set_posts_datetime(app, posts): +def set_posts_datetime(app: Flask, posts: [FlatPages]) -> None: # Ensure each post has a published date # set time for correct date field for post in posts: @@ -151,7 +158,11 @@ def set_posts_datetime(app, posts): default=True, help='If JavaScript should be minified', ) -def build(ctx, css_minify, js_minify): +def build( + ctx: click.Context, + css_minify: bool, # noqa: FBT001 + js_minify: bool, # noqa: FBT001 +) -> None: ctx.invoke(verify) # If verify fails sys.exit(1) will run @@ -205,7 +216,13 @@ def build(ctx, css_minify, js_minify): default=True, help='If JavaScript should be minified', ) -def preview(_ctx, host, port, css_minify, js_minify): +def preview( + _ctx: click.Context, + host: str, + port: int, + css_minify: bool, # noqa: FBT001 + js_minify: bool, # noqa: FBT001 +) -> None: from . import site # reload for tests to refresh app.static_folder # otherwise app.static_folder will be from another test @@ -222,7 +239,7 @@ def preview(_ctx, host, port, css_minify, js_minify): @cli.command('templates', short_help='Create any missing templates') -def templates(): +def templates() -> None: try: copy_missing_templates() except FileNotFoundError: diff --git a/htmd/site.py b/htmd/site.py index 605b30d..2634bfa 100644 --- a/htmd/site.py +++ b/htmd/site.py @@ -1,11 +1,13 @@ +from collections.abc import Iterator import os from pathlib import Path import sys import tomllib +from typing import TypedDict from bs4 import BeautifulSoup from feedwerk.atom import AtomFeed -from flask import abort, Blueprint, Flask, render_template, url_for +from flask import abort, Blueprint, Flask, render_template, Response, url_for from flask_flatpages import FlatPages, pygments_style_defs from flask_frozen import Freezer from htmlmin import minify @@ -15,7 +17,7 @@ this_dir = Path(__file__).parent -def get_project_dir(): +def get_project_dir() -> Path: current_directory = Path.cwd() while True: @@ -103,7 +105,7 @@ def get_project_dir(): app.jinja_env.globals[key] = app.config[key] -def truncate_post_html(post_html): +def truncate_post_html(post_html: str) -> str: return BeautifulSoup(post_html[:255], 'html.parser').prettify() @@ -139,7 +141,7 @@ def truncate_post_html(post_html): @app.after_request -def format_html(response): +def format_html(response: Response) -> Response: if response.mimetype == 'text/html': if app.config.get('PRETTY_HTML', False): response.data = BeautifulSoup( @@ -152,7 +154,7 @@ def format_html(response): @pages.route('//') -def page(path): +def page(path: str) -> Response: try: return render_template(path + '.html', active=path) except TemplateNotFound: @@ -164,18 +166,18 @@ def page(path): # Will end up in the static directory @app.route('/static/pygments.css') -def pygments_css(): +def pygments_css() -> Response: return pygments_style_defs('tango'), 200, {'Content-Type': 'text/css'} @app.route('/') -def index(): +def index() -> Response: latest = sorted(posts, reverse=True, key=lambda p: p.meta.get('published')) return render_template('index.html', active='home', posts=latest[:4]) @app.route('/feed.atom') -def feed(): +def feed() -> Response: name = app.config.get('SITE_NAME') subtitle = app.config.get('SITE_DESCRIPTION') or 'Recent Blog Posts' url = app.config.get('URL') @@ -208,14 +210,14 @@ def feed(): @app.route('/all/') -def all_posts(): +def all_posts() -> Response: latest = sorted(posts, reverse=True, key=lambda p: p.meta.get('published')) return render_template('all_posts.html', active='posts', posts=latest) # If month and day are ints then Flask removes leading zeros @app.route('/////') -def post(year, month, day, path): +def post(year: str, month: str, day:str, path: str) -> Response: if len(year) != 4 or len(month) != 2 or len(day) != 2: # noqa: PLR2004 abort(404) post = posts.get_or_404(path) @@ -225,11 +227,16 @@ def post(year, month, day, path): return render_template('post.html', post=post) -def tag_in_list(list_of_tags, tag): +class TagDict(TypedDict): + tag: str + count: int + + +def tag_in_list(list_of_tags: [TagDict], tag: str) -> bool: return any(i['tag'] == tag for i in list_of_tags) -def increment_tag_count(list_of_tags, tag): +def increment_tag_count(list_of_tags: [TagDict], tag: str) -> [TagDict]: for i in list_of_tags: if i['tag'] == tag: i['count'] += 1 @@ -237,7 +244,7 @@ def increment_tag_count(list_of_tags, tag): @app.route('/tags/') -def all_tags(): +def all_tags() -> Response: tags = [] for post in posts: for tag in post.meta.get('tags', []): @@ -249,7 +256,7 @@ def all_tags(): @app.route('/tags//') -def tag(tag): +def tag(tag: str) -> Response: tagged = [p for p in posts if tag in p.meta.get('tags', [])] sorted_posts = sorted( tagged, @@ -260,7 +267,7 @@ def tag(tag): @app.route('/author//') -def author(author): +def author(author: str) -> Response: author_posts = [p for p in posts if author == p.meta.get('author', '')] sorted_posts = sorted( author_posts, @@ -276,12 +283,12 @@ def author(author): @app.route('/404.html') -def not_found(): +def not_found() -> Response: return render_template('404.html') @app.route('//') -def year_view(year): +def year_view(year: int) -> Response: year = str(year) if len(year) != len('YYYY'): abort(404) @@ -299,7 +306,7 @@ def year_view(year): @app.route('///') -def month_view(year, month): +def month_view(year: str, month: str) -> Response: month_posts = [ p for p in posts if year == p.meta.get('published').strftime('%Y') and month == p.meta.get('published').strftime('%m') @@ -321,7 +328,7 @@ def month_view(year, month): @app.route('////') -def day_view(year, month, day): +def day_view(year: str, month: str, day: str) -> Response: day_posts = [ p for p in posts if year == p.meta.get('published').strftime('%Y') and month == p.meta.get('published').strftime('%m') @@ -340,13 +347,13 @@ def day_view(year, month, day): @app.errorhandler(404) -def page_not_found(_e): +def page_not_found(_e: Exception | int) -> Response: return render_template('404.html'), 404 # Telling Frozen-Flask about routes that are not linked to in templates @freezer.register_generator -def year_view(): # noqa: F811 +def year_view() -> Iterator[dict]: # noqa: F811 for post in posts: yield { 'year': post.meta.get('published').year, @@ -354,7 +361,7 @@ def year_view(): # noqa: F811 @freezer.register_generator -def month_view(): # noqa: F811 +def month_view() -> Iterator[dict]: # noqa: F811 for post in posts: yield { 'month': post.meta.get('published').strftime('%m'), @@ -363,7 +370,7 @@ def month_view(): # noqa: F811 @freezer.register_generator -def day_view(): # noqa: F811 +def day_view() -> Iterator[dict]: # noqa: F811 for post in posts: yield { 'day': post.meta.get('published').strftime('%d'), diff --git a/htmd/utils.py b/htmd/utils.py index 9615afc..5b6635c 100644 --- a/htmd/utils.py +++ b/htmd/utils.py @@ -1,13 +1,14 @@ from importlib.resources import as_file, files from pathlib import Path import shutil +from typing import BinaryIO import click from csscompressor import compress from jsmin import jsmin -def create_directory(name): +def create_directory(name: str) -> Path: directory = Path(name) try: directory.mkdir() @@ -19,7 +20,7 @@ def create_directory(name): return directory -def combine_and_minify_css(static_folder): +def combine_and_minify_css(static_folder: Path) -> None: # Combine and minify all .css files in the static folder css_files = sorted([ f for f in static_folder.iterdir() @@ -41,7 +42,7 @@ def combine_and_minify_css(static_folder): master.write(compress(combined)) -def combine_and_minify_js(static_folder): +def combine_and_minify_js(static_folder: Path) -> None: # Combine and minify all .js files in the static folder js_files = sorted([ f for f in static_folder.iterdir() @@ -66,7 +67,7 @@ def combine_and_minify_js(static_folder): master.write(jsmin(combined)) -def copy_file(source, destination): +def copy_file(source: BinaryIO, destination: Path) -> None: if destination.exists() is False: shutil.copyfile(source, destination) click.echo(click.style(f'{destination} was created.', fg='green')) @@ -75,14 +76,14 @@ def copy_file(source, destination): click.echo(click.style(msg, fg='yellow')) -def copy_missing_templates(): +def copy_missing_templates() -> None: template_dir = files('htmd.example_site') / 'templates' for template_file in sorted(template_dir.iterdir()): file_name = template_file.name copy_file(template_file, Path('templates') / file_name) -def copy_site_file(directory, filename): +def copy_site_file(directory: Path, filename: str) -> None: if directory.name == '': anchor = 'htmd.example_site' else: diff --git a/pyproject.toml b/pyproject.toml index 40b6483..fa02ada 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,12 +43,11 @@ Repository = "https://github.com/siecje/htmd.git" [tool.ruff.lint] ignore = [ - "D100", "D103", "D104", "D203", "D211", "D212", "D213", + "D100", "D101", "D103", "D104", "D203", "D211", "D212", "D213", "INP001", "RET504", "S101", "UP015", - "ANN", ] select = ["ALL"] diff --git a/tests/test_build.py b/tests/test_build.py index 9bb409e..435a748 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -16,7 +16,7 @@ ) -def test_build(): +def test_build() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -25,7 +25,7 @@ def test_build(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_verify_fails(): +def test_build_verify_fails() -> None: expected_output = 'Post "example" does not have field title.\n' runner = CliRunner() with runner.isolated_filesystem(): @@ -36,7 +36,7 @@ def test_build_verify_fails(): assert result.output == expected_output -def test_build_js_minify(): +def test_build_js_minify() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -49,7 +49,7 @@ def test_build_js_minify(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_js_minify_no_js_files(): +def test_build_js_minify_no_js_files() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -58,7 +58,7 @@ def test_build_js_minify_no_js_files(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_no_js_minify(): +def test_build_no_js_minify() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -67,7 +67,7 @@ def test_build_no_js_minify(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_css_minify(): +def test_build_css_minify() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -79,7 +79,7 @@ def test_build_css_minify(): assert 'combined.min.css' in contents -def test_build_no_css_minify(): +def test_build_no_css_minify() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -88,7 +88,7 @@ def test_build_no_css_minify(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_css_minify_no_css_files(): +def test_build_css_minify_no_css_files() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -99,7 +99,7 @@ def test_build_css_minify_no_css_files(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_html_pretty_true(): +def test_build_html_pretty_true() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -118,7 +118,7 @@ def test_build_html_pretty_true(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_html_minify_true(): +def test_build_html_minify_true() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -137,7 +137,7 @@ def test_build_html_minify_true(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_page_404(): +def test_build_page_404() -> None: # Linking to a page that doesn't exist # will cause a 404 status code # and stop the build @@ -165,7 +165,7 @@ def test_build_page_404(): assert result.output == expected_output -def test_build_post_404_invalid_date_year(): +def test_build_post_404_invalid_date_year() -> None: # Linking to a post with incorrect values # for dates will cause 404 and stop the build expected_output = ( @@ -192,7 +192,7 @@ def test_build_post_404_invalid_date_year(): assert result.output == expected_output -def test_build_post_404_invalid_date_month(): +def test_build_post_404_invalid_date_month() -> None: # Linking to a post with incorrect values # for dates will cause 404 and stop the build expected_output = ( @@ -231,7 +231,7 @@ def test_build_post_404_invalid_date_month(): assert result.output == expected_output -def test_build_post_404_invalid_date_day(): +def test_build_post_404_invalid_date_day() -> None: # Linking to a post with incorrect values # for dates will cause 404 and stop the build expected_output = ( @@ -270,7 +270,7 @@ def test_build_post_404_invalid_date_day(): assert result.output == expected_output -def test_build_post_404_different_date(): +def test_build_post_404_different_date() -> None: # Linking to a page with the wrong date # will cause a 404 status code # and stop the build @@ -298,7 +298,7 @@ def test_build_post_404_different_date(): assert result.output == expected_output -def test_build_multiple_posts(): +def test_build_multiple_posts() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) @@ -311,7 +311,7 @@ def test_build_multiple_posts(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_year_404_incorrect(): +def test_build_year_404_incorrect() -> None: # Linking to a page with the wrong date # will cause a 404 status code # and stop the build @@ -339,7 +339,7 @@ def test_build_year_404_incorrect(): assert result.output == expected_output -def test_build_year_404_no_posts(): +def test_build_year_404_no_posts() -> None: # Linking to a page with the wrong date # will cause a 404 status code # and stop the build @@ -367,7 +367,7 @@ def test_build_year_404_no_posts(): assert result.output == expected_output -def test_build_month_404_no_posts(): +def test_build_month_404_no_posts() -> None: # Linking to a page with the wrong date # will cause a 404 status code # and stop the build @@ -395,7 +395,7 @@ def test_build_month_404_no_posts(): assert result.output == expected_output -def test_build_day_404_no_posts(): +def test_build_day_404_no_posts() -> None: # Linking to a page with the wrong date # will cause a 404 status code # and stop the build @@ -423,7 +423,7 @@ def test_build_day_404_no_posts(): assert result.output == expected_output -def test_build_from_sub_directory(): +def test_build_from_sub_directory() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -434,7 +434,7 @@ def test_build_from_sub_directory(): assert re.search(SUCCESS_REGEX, result.output) -def test_build_feed_dot_atom(): +def test_build_feed_dot_atom() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -443,7 +443,7 @@ def test_build_feed_dot_atom(): assert (Path(current_directory) / 'build' / 'feed.atom').is_file -def test_build_updated_time_is_added(): +def test_build_updated_time_is_added() -> None: # If there is no published/updated time then # build will add it # verify that time is not there @@ -492,7 +492,7 @@ def test_build_updated_time_is_added(): assert time_difference.total_seconds() < threshold_seconds -def test_build_published_time_is_added(): +def test_build_published_time_is_added() -> None: # If there is no published/updated time then # build will add it # verify that time is not there @@ -538,7 +538,7 @@ def test_build_published_time_is_added(): assert 'updated' not in ''.join(a_lines) -def test_build_updated_is_added(): +def test_build_updated_is_added() -> None: # If published has a time # and there is no updated then # build will add updated with time @@ -572,7 +572,7 @@ def test_build_updated_is_added(): assert time_difference.total_seconds() < threshold_seconds -def test_build_updated_is_added_once(): +def test_build_updated_is_added_once() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -600,7 +600,7 @@ def test_build_updated_is_added_once(): assert count == 1 -def test_build_without_published(): +def test_build_without_published() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) diff --git a/tests/test_preview.py b/tests/test_preview.py index 185e740..6ec643d 100644 --- a/tests/test_preview.py +++ b/tests/test_preview.py @@ -2,7 +2,7 @@ from htmd.cli import preview, start -def test_preview(): +def test_preview() -> None: runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(start) diff --git a/tests/test_start.py b/tests/test_start.py index 6c24e3f..4bd5338 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -4,7 +4,7 @@ from htmd.cli import start -def test_start(): +def test_start() -> None: runner = CliRunner() expected_output = ( 'templates/ was created.\n' @@ -25,7 +25,7 @@ def test_start(): assert result.output == expected_output -def test_start_all_templates(): +def test_start_all_templates() -> None: runner = CliRunner() expected_output = ( 'templates/ was created.\n' @@ -57,7 +57,7 @@ def test_start_all_templates(): assert result.output == expected_output -def test_start_with_existing_template(): +def test_start_with_existing_template() -> None: runner = CliRunner() with runner.isolated_filesystem(): Path('templates').mkdir() diff --git a/tests/test_templates.py b/tests/test_templates.py index 1ffcb1f..ab9ef52 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -2,7 +2,7 @@ from htmd.cli import start, templates -def test_templates(): +def test_templates() -> None: expected_output = ( 'templates/404.html was created.\n' 'templates/_layout.html already exists and was not created.\n' @@ -25,7 +25,7 @@ def test_templates(): assert result.output == expected_output -def test_templates_without_folder(): +def test_templates_without_folder() -> None: expected_output = 'templates/ directory not found.\n' runner = CliRunner() with runner.isolated_filesystem(): diff --git a/tests/test_verify.py b/tests/test_verify.py index c4f1349..81c0de7 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -6,7 +6,7 @@ from utils import remove_fields_from_example_post -def test_verify(): +def test_verify() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -16,7 +16,7 @@ def test_verify(): assert result.output == expected_output -def test_verify_author_missing(): +def test_verify_author_missing() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -30,7 +30,7 @@ def test_verify_author_missing(): assert result.output == expected_output -def test_verify_title_missing(): +def test_verify_title_missing() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -44,7 +44,7 @@ def test_verify_title_missing(): assert result.output == expected_output -def test_verify_published_missing(): +def test_verify_published_missing() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -60,7 +60,7 @@ def test_verify_published_missing(): assert result.exit_code == 0 -def test_verify_published_invalid_year(): +def test_verify_published_invalid_year() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -83,7 +83,7 @@ def test_verify_published_invalid_year(): assert result.output == expected_output -def test_verify_published_invalid_month(): +def test_verify_published_invalid_month() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -106,7 +106,7 @@ def test_verify_published_invalid_month(): assert result.output == expected_output -def test_verify_published_invalid_day(): +def test_verify_published_invalid_day() -> None: runner = CliRunner() with runner.isolated_filesystem(): runner.invoke(start) @@ -129,7 +129,7 @@ def test_verify_published_invalid_day(): assert result.output == expected_output -def test_verify_site_name_empty(): +def test_verify_site_name_empty() -> None: expected_output = ( 'All posts are correctly formatted.\n' '[site] name is not set in config.toml.\n' @@ -156,7 +156,7 @@ def test_verify_site_name_empty(): assert result.output == expected_output -def test_verify_site_name_missing(): +def test_verify_site_name_missing() -> None: expected_output = ( 'All posts are correctly formatted.\n' '[site] name is not set in config.toml.\n' @@ -178,7 +178,7 @@ def test_verify_site_name_missing(): assert result.output == expected_output -def test_verify_no_config(): +def test_verify_no_config() -> None: expected_output = 'Can not find config.toml\n' runner = CliRunner() with runner.isolated_filesystem(): diff --git a/tests/utils.py b/tests/utils.py index e33dcd8..486b318 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ from pathlib import Path -def remove_fields_from_example_post(field_names): +def remove_fields_from_example_post(field_names: (str,)) -> None: with (Path('posts') / 'example.md').open('r') as post: lines = post.readlines() with (Path('posts') / 'example.md').open('w') as post: