From c7d80bf42c4a43b504a2ce80ae0f4501007b748f Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 8 Dec 2022 20:41:12 +0100 Subject: type hints for fietsboek.util --- .mypy.ini | 3 +++ fietsboek/util.py | 66 +++++++++++++++++------------------------------ fietsboek/views/upload.py | 3 ++- 3 files changed, 29 insertions(+), 43 deletions(-) diff --git a/.mypy.ini b/.mypy.ini index 79f0c44..ed220e3 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -10,5 +10,8 @@ ignore_missing_imports = True [mypy-sqlalchemy.*] ignore_missing_imports = True +[mypy-webob.*] +ignore_missing_imports = True + [mypy-zope.*] ignore_missing_imports = True diff --git a/fietsboek/util.py b/fietsboek/util.py index a500d1e..e4a66cf 100644 --- a/fietsboek/util.py +++ b/fietsboek/util.py @@ -4,6 +4,7 @@ import re import os import unicodedata import secrets +from typing import Optional # Compat for Python < 3.9 import importlib_resources @@ -11,9 +12,12 @@ import babel import markdown import bleach import gpxpy +import webob +import sqlalchemy from pyramid.i18n import TranslationString as _ from pyramid.httpexceptions import HTTPBadRequest +from pyramid.request import Request from markupsafe import Markup from sqlalchemy import select @@ -47,23 +51,21 @@ _windows_device_files = ( ) -def safe_markdown(md_source): +def safe_markdown(md_source: str) -> Markup: """Transform a markdown document into a safe HTML document. This uses ``markdown`` to first parse the markdown source into HTML, and then ``bleach`` to strip any disallowed HTML tags. :param md_source: The markdown source. - :type md_source: str :return: The safe HTML transformed version. - :rtype: Markup """ html = markdown.markdown(md_source, output_format='html') html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) return Markup(html) -def fix_iso_timestamp(timestamp): +def fix_iso_timestamp(timestamp: str) -> str: """Fixes an ISO timestamp to make it parseable by :meth:`datetime.datetime.fromisoformat`. @@ -71,24 +73,21 @@ def fix_iso_timestamp(timestamp): it with '+00:00'. :param timestamp: The timestamp to fix. - :type timestamp: str :return: The fixed timestamp. - :rtype: str """ if timestamp.endswith('Z'): return timestamp[:-1] + '+00:00' return timestamp -def round_timedelta_to_multiple(value, multiples): +def round_timedelta_to_multiple( + value: datetime.timedelta, multiples: datetime.timedelta +) -> datetime.timedelta: """Round the timedelta `value` to be a multiple of `multiples`. :param value: The value to be rounded. - :type value: datetime.timedelta :param multiples: The size of each multiple. - :type multiples: datetime.timedelta :return: The rounded value. - :rtype: datetime.timedelta """ lower = value.total_seconds() // multiples.total_seconds() * multiples.total_seconds() second_offset = value.total_seconds() - lower @@ -99,16 +98,14 @@ def round_timedelta_to_multiple(value, multiples): return datetime.timedelta(seconds=lower) + multiples -def guess_gpx_timezone(gpx): +def guess_gpx_timezone(gpx: gpxpy.gpx.GPX) -> datetime.tzinfo: """Guess which timezone the GPX file was recorded in. This looks at a few timestamps to see if they have timezone information attached, including some known GPX extensions. :param gpx: The parsed GPX file to analyse. - :type gpx: gpxpy.GPX :return: The timezone information. - :rtype: datetime.timezone """ time_bounds = gpx.get_time_bounds() times = [ @@ -152,7 +149,7 @@ def guess_gpx_timezone(gpx): return datetime.timezone.utc -def tour_metadata(gpx_data): +def tour_metadata(gpx_data: str) -> dict: """Calculate the metadata of the tour. Returns a dict with ``length``, ``uphill``, ``downhill``, ``moving_time``, @@ -160,9 +157,7 @@ def tour_metadata(gpx_data): ``end_time``. :param gpx_data: The GPX data of the tour. - :type gpx_data: str :return: A dictionary with the computed values. - :rtype: dict """ gpx = gpxpy.parse(gpx_data) timezone = guess_gpx_timezone(gpx) @@ -186,46 +181,44 @@ def tour_metadata(gpx_data): } -def mps_to_kph(mps): +def mps_to_kph(mps: float) -> float: """Converts meters/second to kilometers/hour. :param mps: Input meters/second. - :type mps: float :return: The converted km/h value. - :rtype: float """ return mps / 1000 * 60 * 60 -def month_name(request, month): +def month_name(request: Request, month: int) -> str: """Returns the localized name for the month with the given number. :param request: The pyramid request. - :type request: pyramid.request.Request :param month: Number of the month, 1 = January. - :type month: int :return: The localized month name. - :rtype: str """ assert 1 <= month <= 12 locale = babel.Locale.parse(request.localizer.locale_name) return locale.months["stand-alone"]["wide"][month] -def random_link_secret(nbytes=20): +def random_link_secret(nbytes: int = 20) -> str: """Safely generates a secret suitable for the link share. The returned string consists of characters that are safe to use in a URL. :param nbytes: Number of random bytes to use. - :type nbytes: int :return: A randomly drawn string. - :rtype: str """ return secrets.token_urlsafe(nbytes) -def retrieve_multiple(dbsession, model, params, name): +def retrieve_multiple( + dbsession: "sqlalchemy.orm.session.Session", + model: type, + params: "webob.multidict.NestedMultiDict", + name: str, +) -> list: """Parses a reply to retrieve multiple database objects. This is usable for arrays sent by HTML forms, for example to retrieve all @@ -237,15 +230,10 @@ def retrieve_multiple(dbsession, model, params, name): :raises pyramid.httpexceptions.HTTPBadRequest: If an object could not be found. :param dbsession: The database session. - :type dbsession: sqlalchemy.orm.session.Session :param model: The model class to retrieve. - :type model: class :param params: The form parameters. - :type params: webob.multidict.NestedMultiDict :param name: Name of the parameter to look for. - :type name: str :return: A list of elements found. - :rtype: list[model] """ objects = [] for obj_id in params.getall(name): @@ -259,7 +247,7 @@ def retrieve_multiple(dbsession, model, params, name): return objects -def check_password_constraints(password, repeat_password=None): +def check_password_constraints(password: str, repeat_password: Optional[str] = None): """Verifies that the password constraints match for the given password. This is usually also verified client-side, but for people that bypass the @@ -273,9 +261,7 @@ def check_password_constraints(password, repeat_password=None): :class:`~pyramid.i18n.TranslationString` with the message of why the verification failed. :param password: The password which to verify. - :type password: str :param repeat_password: The password repeat. - :type repeat_password: str """ if repeat_password is not None: if repeat_password != password: @@ -284,7 +270,7 @@ def check_password_constraints(password, repeat_password=None): raise ValueError(_("password_constraint.length")) -def read_localized_resource(locale_name, path, raise_on_error=False): +def read_localized_resource(locale_name: str, path: str, raise_on_error: bool = False) -> str: """Reads a localized resource. Localized resources are located in the ``fietsboek/locale/**`` directory. @@ -293,13 +279,11 @@ def read_localized_resource(locale_name, path, raise_on_error=False): If the resource could not be found, a placeholder string is returned instead. :param locale_name: Name of the locale. - :type locale_name: str + :param path: Path of the resource. :param raise_on_error: Raise an error instead of returning a placeholder. - :type raise_on_error: bool :raises FileNotFoundError: If the path could not be found and ``raise_on_error`` is ``True``. :return: The text content of the resource. - :rtype: str """ locales = [locale_name] # Second chance: If the locale is a specific form of a more general @@ -319,7 +303,7 @@ def read_localized_resource(locale_name, path, raise_on_error=False): return f"{locale_name}:{path}" -def secure_filename(filename): +def secure_filename(filename: str) -> str: r"""Pass it a filename and it will return a secure version of it. This filename can then safely be stored on a regular file system and passed to :func:`os.path.join`. The filename returned is an ASCII only string @@ -339,9 +323,7 @@ def secure_filename(filename): generate a random filename if the function returned an empty one. :param filename: the filename to secure - :type filename: str :return: The secure filename. - :rtype: str """ # Taken from # https://github.com/pallets/werkzeug/blob/main/src/werkzeug/utils.py diff --git a/fietsboek/views/upload.py b/fietsboek/views/upload.py index f63f45d..1fb30e2 100644 --- a/fietsboek/views/upload.py +++ b/fietsboek/views/upload.py @@ -108,6 +108,7 @@ def finish_upload(request): date = gpx.time or gpx.get_time_bounds().start_time or datetime.datetime.now() date = date.astimezone(timezone) tz_offset = timezone.utcoffset(date) + tz_offset = 0 if tz_offset is None else tz_offset.total_seconds() track_name = "" for track in gpx.tracks: if track.name: @@ -118,7 +119,7 @@ def finish_upload(request): 'preview_id': upload.id, 'upload_title': gpx.name or track_name, 'upload_date': date, - 'upload_date_tz': int(tz_offset.total_seconds() // 60), + 'upload_date_tz': int(tz_offset // 60), 'upload_visibility': Visibility.PRIVATE, 'upload_type': TrackType.ORGANIC, 'upload_description': gpx.description, -- cgit v1.2.3