Skip to content

Commit

Permalink
Fix typehints with mypy
Browse files Browse the repository at this point in the history
  • Loading branch information
Siecje committed Jan 25, 2024
1 parent dfe992e commit 2db032f
Show file tree
Hide file tree
Showing 18 changed files with 272 additions and 54 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1
with:
src: "./htmd"
build:
runs-on: ubuntu-latest
strategy:
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ $ ../venv/bin/htmd start
$ ../venv/bin/htmd build
```

### Running mypy

```shell
$ venv/bin/python -m pip install mypy
$ venv/bin/python -m mypy htmd typehints
```

### Running the tests

```shell
Expand Down
10 changes: 7 additions & 3 deletions htmd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import click
from flask import Flask
from flask_flatpages import FlatPages
from flask_flatpages import FlatPages, Page

from .utils import (
combine_and_minify_css,
Expand Down Expand Up @@ -95,7 +95,7 @@ def verify() -> None:

def set_post_time(
app: Flask,
post: FlatPages,
post: Page,
field: str,
date_time: datetime.datetime,
) -> None:
Expand All @@ -120,7 +120,7 @@ def set_post_time(
file.write(line)


def set_posts_datetime(app: Flask, posts: [FlatPages]) -> None:
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:
Expand Down Expand Up @@ -170,9 +170,11 @@ def build(
app = site.app

if css_minify:
assert app.static_folder is not None
combine_and_minify_css(Path(app.static_folder))

if js_minify:
assert app.static_folder is not None
combine_and_minify_js(Path(app.static_folder))

if css_minify or js_minify:
Expand Down Expand Up @@ -230,9 +232,11 @@ def preview(
app = site.app

if css_minify:
assert app.static_folder is not None
combine_and_minify_css(Path(app.static_folder))

if js_minify:
assert app.static_folder is not None
combine_and_minify_js(Path(app.static_folder))

app.run(debug=True, host=host, port=port)
Expand Down
100 changes: 51 additions & 49 deletions htmd/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from pathlib import Path
import sys
import tomllib
import typing

from bs4 import BeautifulSoup
from feedwerk.atom import AtomFeed
from flask import abort, Blueprint, Flask, render_template, Response, url_for
from flask.typing import ResponseReturnValue
from flask_flatpages import FlatPages, pygments_style_defs
from flask_frozen import Freezer
from htmlmin import minify
Expand Down Expand Up @@ -56,41 +58,41 @@ def get_project_dir() -> Path:
# Flask configs are flat, config.toml is not
# Define the configuration keys and their default values
# 'Flask config': [section, key, default]
config_keys = {
'SITE_NAME': ['site', 'name', ''],
'SITE_URL': ['site', 'url', ''],
'SITE_LOGO': ['site', 'logo', ''],
'SITE_DESCRIPTION': ['site', 'description', ''],
'SITE_TWITTER': ['site', 'twitter', ''],
'SITE_FACEBOOK': ['site', 'facebook', ''],
'FACEBOOK_APP_ID': ['site', 'facebook_app_id', ''],
'STATIC_FOLDER': ['folders', 'static', 'static'],
'POSTS_FOLDER': ['folders', 'posts', 'posts'],
'PAGES_FOLDER': ['folders', 'pages', 'pages'],
'BUILD_FOLDER': ['folders', 'build', 'build'],
'POSTS_EXTENSION': ['posts', 'extension', '.md'],
'PRETTY_HTML': ['html', 'pretty', False],
'MINIFY_HTML': ['html', 'minify', False],
'SHOW_AUTHOR': ['author', 'show', True],
'DEFAULT_AUTHOR': ['author', 'default_name', ''],
'DEFAULT_AUTHOR_TWITTER': ['author', 'default_twitter', ''],
'DEFAULT_AUTHOR_FACEBOOK': ['author', 'default_facebook', ''],
config_keys : dict[str, tuple[str, str, typing.Any]] = {
'SITE_NAME': ('site', 'name', ''),
'SITE_URL': ('site', 'url', ''),
'SITE_LOGO': ('site', 'logo', ''),
'SITE_DESCRIPTION': ('site', 'description', ''),
'SITE_TWITTER': ('site', 'twitter', ''),
'SITE_FACEBOOK': ('site', 'facebook', ''),
'FACEBOOK_APP_ID': ('site', 'facebook_app_id', ''),
'STATIC_FOLDER': ('folders', 'static', 'static'),
'POSTS_FOLDER': ('folders', 'posts', 'posts'),
'PAGES_FOLDER': ('folders', 'pages', 'pages'),
'BUILD_FOLDER': ('folders', 'build', 'build'),
'POSTS_EXTENSION': ('posts', 'extension', '.md'),
'PRETTY_HTML': ('html', 'pretty', False),
'MINIFY_HTML': ('html', 'minify', False),
'SHOW_AUTHOR': ('author', 'show', True),
'DEFAULT_AUTHOR': ('author', 'default_name', ''),
'DEFAULT_AUTHOR_TWITTER': ('author', 'default_twitter', ''),
'DEFAULT_AUTHOR_FACEBOOK': ('author', 'default_facebook', ''),
}

# Update app.config using the configuration keys
for flask_key, (table, key, default) in config_keys.items():
app.config[flask_key] = htmd_config.get(table, {}).get(key, default)

assert app.static_folder is not None

# To avoid full paths in config.toml
app.config['FLATPAGES_ROOT'] = (
project_dir / app.config.get('POSTS_FOLDER')
project_dir / app.config['POSTS_FOLDER']
)
app.config['FREEZER_DESTINATION'] = (
project_dir / app.config.get('BUILD_FOLDER')
project_dir / app.config['BUILD_FOLDER']
)
app.config['FREEZER_REMOVE_EXTRA_FILES'] = False
app.config['FLATPAGES_EXTENSION'] = app.config.get('POSTS_EXTENSION')
app.config['FLATPAGES_EXTENSION'] = app.config['POSTS_EXTENSION']

app.config['INCLUDE_CSS'] = 'combined.min.css' in os.listdir(app.static_folder)
app.config['INCLUDE_JS'] = 'combined.min.js' in os.listdir(app.static_folder)
Expand All @@ -112,9 +114,9 @@ def truncate_post_html(post_html: str) -> str:


# Include current htmd site templates
app.jinja_loader = ChoiceLoader([
app.jinja_loader = ChoiceLoader([ # type: ignore[assignment]
FileSystemLoader(project_dir / 'templates/'),
app.jinja_loader,
app.jinja_loader, # type: ignore[list-item]
])

MONTHS = {
Expand All @@ -135,12 +137,12 @@ def truncate_post_html(post_html: str) -> str:
pages = Blueprint(
'pages',
__name__,
template_folder=project_dir / app.config.get('PAGES_FOLDER'),
template_folder=project_dir / app.config['PAGES_FOLDER'],
)


@app.after_request
def format_html(response: Response) -> Response:
def format_html(response: Response) -> ResponseReturnValue:
if response.mimetype == 'text/html':
if app.config.get('PRETTY_HTML', False):
response.data = BeautifulSoup(
Expand All @@ -153,7 +155,7 @@ def format_html(response: Response) -> Response:


@pages.route('/<path:path>/')
def page(path: str) -> Response:
def page(path: str) -> ResponseReturnValue:
try:
return render_template(path + '.html', active=path)
except TemplateNotFound:
Expand All @@ -165,18 +167,18 @@ def page(path: str) -> Response:

# Will end up in the static directory
@app.route('/static/pygments.css')
def pygments_css() -> Response:
def pygments_css() -> ResponseReturnValue:
return pygments_style_defs('tango'), 200, {'Content-Type': 'text/css'}


@app.route('/')
def index() -> Response:
def index() -> ResponseReturnValue:
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() -> Response:
def feed() -> ResponseReturnValue:
name = app.config.get('SITE_NAME')
subtitle = app.config.get('SITE_DESCRIPTION') or 'Recent Blog Posts'
url = app.config.get('URL')
Expand Down Expand Up @@ -209,14 +211,14 @@ def feed() -> Response:


@app.route('/all/')
def all_posts() -> Response:
def all_posts() -> ResponseReturnValue:
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('/<year>/<month>/<day>/<path:path>/')
def post(year: str, month: str, day:str, path: str) -> Response:
def post(year: str, month: str, day:str, path: str) -> ResponseReturnValue:
if len(year) != 4 or len(month) != 2 or len(day) != 2: # noqa: PLR2004
abort(404)
post = posts.get_or_404(path)
Expand All @@ -227,8 +229,8 @@ def post(year: str, month: str, day:str, path: str) -> Response:


@app.route('/tags/')
def all_tags() -> Response:
tag_counts: {str: int} = {}
def all_tags() -> ResponseReturnValue:
tag_counts: dict[str, int] = {}
for post in posts:
for tag in post.meta.get('tags', []):
if tag not in tag_counts:
Expand All @@ -238,7 +240,7 @@ def all_tags() -> Response:


@app.route('/tags/<string:tag>/')
def tag(tag: str) -> Response:
def tag(tag: str) -> ResponseReturnValue:
tagged = [p for p in posts if tag in p.meta.get('tags', [])]
sorted_posts = sorted(
tagged,
Expand All @@ -249,7 +251,7 @@ def tag(tag: str) -> Response:


@app.route('/author/<author>/')
def author(author: str) -> Response:
def author(author: str) -> ResponseReturnValue:
author_posts = [p for p in posts if author == p.meta.get('author', '')]
sorted_posts = sorted(
author_posts,
Expand All @@ -265,17 +267,17 @@ def author(author: str) -> Response:


@app.route('/404.html')
def not_found() -> Response:
def not_found() -> ResponseReturnValue:
return render_template('404.html')


@app.route('/<int:year>/')
def year_view(year: int) -> Response:
year = str(year)
if len(year) != len('YYYY'):
def year_view(year: int) -> ResponseReturnValue:
year_str = str(year)
if len(year_str) != len('YYYY'):
abort(404)
year_posts = [
p for p in posts if year == p.meta.get('published', []).strftime('%Y')
p for p in posts if year_str == p.meta.get('published', []).strftime('%Y')
]
if not year_posts:
abort(404)
Expand All @@ -284,11 +286,11 @@ def year_view(year: int) -> Response:
reverse=False,
key=lambda p: p.meta.get('published'),
)
return render_template('year.html', year=year, posts=sorted_posts)
return render_template('year.html', year=year_str, posts=sorted_posts)


@app.route('/<year>/<month>/')
def month_view(year: str, month: str) -> Response:
def month_view(year: str, month: str) -> ResponseReturnValue:
month_posts = [
p for p in posts if year == p.meta.get('published').strftime('%Y')
and month == p.meta.get('published').strftime('%m')
Expand All @@ -310,7 +312,7 @@ def month_view(year: str, month: str) -> Response:


@app.route('/<year>/<month>/<day>/')
def day_view(year: str, month: str, day: str) -> Response:
def day_view(year: str, month: str, day: str) -> ResponseReturnValue:
day_posts = [
p for p in posts if year == p.meta.get('published').strftime('%Y')
and month == p.meta.get('published').strftime('%m')
Expand All @@ -329,20 +331,20 @@ def day_view(year: str, month: str, day: str) -> Response:


@app.errorhandler(404)
def page_not_found(_e: Exception | int) -> Response:
def page_not_found(_e: Exception | int) -> ResponseReturnValue:
return render_template('404.html'), 404


# Telling Frozen-Flask about routes that are not linked to in templates
@freezer.register_generator
@freezer.register_generator # type: ignore[no-redef]
def year_view() -> Iterator[dict]: # noqa: F811
for post in posts:
yield {
'year': post.meta.get('published').year,
}


@freezer.register_generator
@freezer.register_generator # type: ignore[no-redef]
def month_view() -> Iterator[dict]: # noqa: F811
for post in posts:
yield {
Expand All @@ -351,7 +353,7 @@ def month_view() -> Iterator[dict]: # noqa: F811
}


@freezer.register_generator
@freezer.register_generator # type: ignore[no-redef]
def day_view() -> Iterator[dict]: # noqa: F811
for post in posts:
yield {
Expand Down
3 changes: 1 addition & 2 deletions htmd/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from importlib.resources import as_file, files
from pathlib import Path
import shutil
from typing import BinaryIO

import click
from csscompressor import compress
Expand Down Expand Up @@ -67,7 +66,7 @@ def combine_and_minify_js(static_folder: Path) -> None:
master.write(jsmin(combined))


def copy_file(source: BinaryIO, destination: Path) -> None:
def copy_file(source: Path, destination: Path) -> None:
if destination.exists() is False:
shutil.copyfile(source, destination)
click.echo(click.style(f'{destination} was created.', fg='green'))
Expand Down
4 changes: 4 additions & 0 deletions typehints/csscompressor/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__all__ = ['compress', 'compress_partitioned']

def compress(css: str, max_linelen: int = 0, preserve_exclamation_comments: bool = True) -> str: ...
def compress_partitioned(css: str, max_linelen: int = 0, max_rules_per_file: int = 4000, preserve_exclamation_comments: bool = True) -> list[str]: ...
Empty file added typehints/feedwerk/__init__.pyi
Empty file.
41 changes: 41 additions & 0 deletions typehints/feedwerk/_compat.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from _typeshed import Incomplete
from io import BytesIO as BytesIO, StringIO

PY2: Incomplete
WIN: Incomplete
unichr = chr
text_type = str
string_types: Incomplete
integer_types: Incomplete
iterkeys: Incomplete
itervalues: Incomplete
iteritems: Incomplete
iterlists: Incomplete
iterlistvalues: Incomplete
int_to_byte: Incomplete
iter_bytes: Incomplete

def reraise(tp, value, tb: Incomplete | None = None) -> None: ...

fix_tuple_repr: Incomplete
implements_iterator: Incomplete
implements_to_string: Incomplete
implements_bool: Incomplete
native_string_result: Incomplete
imap = map
izip = zip
ifilter = filter
range_type = range
NativeStringIO = StringIO

def make_literal_wrapper(reference): ...
def normalize_string_tuple(tup): ...

try_coerce_native: Incomplete
wsgi_get_bytes: Incomplete

def wsgi_decoding_dance(s, charset: str = 'utf-8', errors: str = 'replace'): ...
def wsgi_encoding_dance(s, charset: str = 'utf-8', errors: str = 'replace'): ...
def to_bytes(x, charset=..., errors: str = 'strict'): ...
def to_native(x, charset=..., errors: str = 'strict'): ...
def to_unicode(x, charset=..., errors: str = 'strict', allow_none_charset: bool = False): ...
Loading

0 comments on commit 2db032f

Please sign in to comment.