From b456196f9a9d200bb2003c847836621fd39b43a5 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 20 Nov 2025 22:38:59 +0100 Subject: add docstrings & fix lint --- fietsboek/pdf.py | 69 ++++++++++++++++++++++++++++++++++++++++++----- fietsboek/trackmap.py | 4 ++- fietsboek/views/detail.py | 5 ++++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/fietsboek/pdf.py b/fietsboek/pdf.py index a6b40c9..97e1d7b 100644 --- a/fietsboek/pdf.py +++ b/fietsboek/pdf.py @@ -1,9 +1,23 @@ +"""PDF generation for tracks. + +This module implements functionality that renders a PDF overview for a track. +The PDF overview consists of a map using OSM tiles, and a table with the +computed metadata. + +PDF generation is done using Typst_ via the `Python bindings`_. Typst provides +layouting and good typography without too much effort from our side. The Typst +file is generated from a Jinja2 template, saved to a temporary directory +together with the track image, and then compiled. + +.. _Typst: https://typst.app/ +.. _Python bindings: https://pypi.org/project/typst/ +""" + import html.parser import importlib.resources import io import logging import tempfile -from dataclasses import dataclass from itertools import chain from pathlib import Path @@ -45,6 +59,13 @@ TO_ESCAPE = { class HtmlToTypst(html.parser.HTMLParser): + """A parser that converts HTML to Typst syntax. + + This is adjusted for the HTML output that the markdown converted produces. + It will not work for arbitrary HTML. + """ + + # pylint: disable=too-many-branches def __init__(self, out): super().__init__() self.out = out @@ -102,22 +123,46 @@ class HtmlToTypst(html.parser.HTMLParser): def typst_string(value: str) -> str: + """Serializes a string to a string that can be embedded in Typst source. + + This wraps the value in quotes, and escapes all characters. + + :param value: The value to serialize. + :return: The serialized string, ready to be embedded. + """ return '"' + "".join(f"\\u{{{ord(char):x}}}" for char in value) + '"' def typst_escape(value: str) -> str: + """Escapes Typst markup in the given value. + + :param value: The value to escape. + :return: The value with formatting characters escaped. + """ return "".join("\\" + char if char in TO_ESCAPE else char for char in value) def md_to_typst(value: str) -> str: - html = util.safe_markdown(value).unescape() + """Convert Markdown-formatted text to Typst source code. + + :param value: The Markdown source to convert. + :return: The Typst code. + """ + html_source = util.safe_markdown(value).unescape() buffer = io.StringIO() parser = HtmlToTypst(buffer) - parser.feed(html) + parser.feed(html_source) return buffer.getvalue() def draw_map(track: Track, requester: TileRequester, tile_layer: TileLayerConfig, outfile: Path): + """Renders the track map. + + :param track: The track. + :param requester: The requester which is used to retrieve tiles. + :param tile_layer: The OSM tile layer configuration. + :param outfile: Path to the output file. + """ map_image = trackmap.render( track.path(), tile_layer, @@ -130,7 +175,19 @@ def draw_map(track: Track, requester: TileRequester, tile_layer: TileLayerConfig def generate( track: Track, requester: TileRequester, tile_layer: TileLayerConfig, localizer: Localizer ) -> bytes: - """Generate a PDF representation for the given track.""" + """Generate a PDF representation for the given track. + + :param track: The track for which to generate a PDF overview. + :param requester: The tile requester to render the track map. + :param tile_layer: The tile layer to use for the track map. + :param localizer: The localizer. + :return: The PDF bytes. + """ + # Yes, we could use f-strings, but I don't like embedding the huge + # expressions below in f-strings: + # pylint: disable=consider-using-f-string + # And this is a false positive for typst.compile: + # pylint: disable=no-member env = jinja2.Environment( loader=jinja2.PackageLoader("fietsboek", "pdf-assets"), autoescape=False, @@ -173,9 +230,9 @@ def generate( "description": track.description, } - with tempfile.TemporaryDirectory(prefix=TEMP_PREFIX) as temp_dir: + with tempfile.TemporaryDirectory(prefix=TEMP_PREFIX) as temp_dir_name: + temp_dir = Path(temp_dir_name) LOGGER.debug("New PDF generation in %s", temp_dir) - temp_dir = Path(temp_dir) font_data = importlib.resources.read_binary("fietsboek", "pdf-assets/Nunito.ttf") (temp_dir / "Nunito.ttf").write_bytes(font_data) diff --git a/fietsboek/trackmap.py b/fietsboek/trackmap.py index 4280e0c..c767852 100644 --- a/fietsboek/trackmap.py +++ b/fietsboek/trackmap.py @@ -119,7 +119,9 @@ class TrackMapRenderer: draw.line(coords, fill=self.color, width=self.line_width, joint="curve") -def render(track: geo.Path, layer: TileLayerConfig, requester: TileRequester, size: (int, int) = (300, 300)) -> Image.Image: +def render( + track: geo.Path, layer: TileLayerConfig, requester: TileRequester, size: (int, int) = (300, 300) +) -> Image.Image: """Shorthand to construct a :class:`TrackMapRenderer` and render the preview. :param track: Track to render. diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index b1d3c74..16c896a 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -288,6 +288,11 @@ def track_map(request: Request): @view_config(route_name="track-pdf", permission="track.view") def track_pdf(request: Request): + """Endpoint to provide the track's PDF overview. + + :param request: The pyramid request. + :return: The HTTP response. + """ loader: ITileRequester = request.registry.getUtility(ITileRequester) layer = request.config.public_tile_layers()[0] pdf_bytes = pdf.generate(request.context, loader, layer, request.localizer) -- cgit v1.2.3