Skip to content

Commit 19b6873

Browse files
authored
Make static type checking work (#578)
* Type hints in undodb * api.resources.emit * Add future import * More typing fixes * More type fixes * type fixes in image * More type fixes * Fix tets * Add missing import * More type fixes * More type fixes * More type fixes * One more fix for today. * More type fixes * More fixes * More fixes * Fix failing test * Fix types in trees resouce * Fix types in dbmanager * Fix regression in trees * More fixes * Fixes in metadata * More type fixes * More fixes * More fixes * 1 more fix * Fixes in resources.util * Fixes in search.indexer * Ignore two errors * Remove stray imports, fix 1 error * Add mypy to CI * Try to fix workflow * Final type fixes * Try to fix type * fix image
1 parent d227273 commit 19b6873

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+454
-318
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,7 @@ jobs:
2525
pip install -r requirements-dev.txt
2626
pip install .[ai]
2727
pip list
28+
- name: Run type checker
29+
run: mypy --ignore-missing-imports --exclude build .
2830
- name: Test with pytest
2931
run: pytest

alembic_users/env.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# type: ignore
2+
13
import os
24
from logging.config import fileConfig
35

gramps_webapi/__main__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131
from threading import Thread
3232

3333
import click
34-
import waitress
34+
import waitress # type: ignore
3535
import webbrowser
3636

37-
from .api.search import get_search_indexer
37+
from .api.search import get_search_indexer, get_semantic_search_indexer
3838
from .api.util import get_db_manager, list_trees, close_db
3939
from .app import create_app
4040
from .auth import add_user, delete_user, fill_tree, user_db
@@ -213,7 +213,10 @@ def search(ctx, tree, semantic):
213213
tree = dbmgr.dirname
214214
with app.app_context():
215215
ctx.obj["db_manager"] = get_db_manager(tree=tree)
216-
ctx.obj["search_indexer"] = get_search_indexer(tree=tree, semantic=semantic)
216+
if semantic:
217+
ctx.obj["search_indexer"] = get_semantic_search_indexer(tree=tree)
218+
else:
219+
ctx.obj["search_indexer"] = get_search_indexer(tree=tree)
217220

218221

219222
def progress_callback_count(

gramps_webapi/api/export.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
"""Functions for exporting a database."""
2323

24+
from __future__ import annotations
25+
2426
import mimetypes
2527
import os
2628
import uuid
@@ -42,6 +44,7 @@
4244
)
4345
from gramps.gen.user import User
4446
from gramps.gen.utils.resourcepath import ResourcePath
47+
from torch import Value
4548

4649
from ..const import DISABLED_EXPORTERS
4750
from .util import UserTaskProgress, abort_with_message, get_locale_for_language
@@ -188,7 +191,7 @@ def apply_proxy(self, proxy_name, dbase):
188191
return dbase
189192

190193

191-
def get_exporters(extension: str = None):
194+
def get_exporters(extension: str | None = None):
192195
"""Extract and return list of exporters."""
193196
exporters = []
194197
plugin_manager = BasePluginManager.get_instance()
@@ -267,6 +270,8 @@ def run_export(
267270
):
268271
"""Generate the export."""
269272
export_path = current_app.config.get("EXPORT_DIR")
273+
if not export_path:
274+
raise abort_with_message(500, "EXPORT_DIR not set in configuration")
270275
os.makedirs(export_path, exist_ok=True)
271276
file_name = f"{uuid.uuid4()}.{extension}"
272277
file_path = os.path.join(export_path, file_name)

gramps_webapi/api/file.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@ def __init__(self, handle, db_handle: DbReadBase):
5252
self.path = self.media.path
5353
self.checksum = self.media.checksum
5454

55-
def _get_media_object(self) -> Optional[Media]:
55+
def _get_media_object(self) -> Media:
5656
"""Get the media object from the database."""
5757
try:
5858
return self.db_handle.get_media_from_handle(self.handle)
5959
except HandleError:
60-
return None
60+
abort_with_message(404, "Media object not found")
61+
raise # unreachable - for type checker
6162

6263
def get_file_object(self) -> BinaryIO:
6364
"""Return a binary file object."""

gramps_webapi/api/html.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323

2424
from typing import Callable, Optional
2525

26-
import bleach
27-
from bleach.css_sanitizer import CSSSanitizer
26+
import bleach # type: ignore
27+
from bleach.css_sanitizer import CSSSanitizer # type: ignore
2828
from gramps.gen.errors import HandleError
2929
from gramps.gen.lib import Note, NoteType, StyledText
3030
from gramps.plugins.lib.libhtml import Html

gramps_webapi/api/image.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import ffmpeg
3030
from pdf2image import convert_from_path
3131
from PIL import Image, ImageOps
32+
from PIL.Image import Image as ImageType
3233
from pkg_resources import resource_filename
3334

3435
from gramps_webapi.const import MIME_PDF
@@ -37,12 +38,13 @@
3738
from .util import abort_with_message
3839

3940

40-
def image_thumbnail(image: Image, size: int, square: bool = False) -> Image:
41+
def image_thumbnail(image: ImageType, size: int, square: bool = False) -> ImageType:
4142
"""Return a thumbnail of `size` (longest side) for the image.
4243
4344
If `square` is true, the image is cropped to a centered square.
4445
"""
4546
img = ImageOps.exif_transpose(image)
47+
assert img is not None, "img is None" # for type checker. Can't happen in practice.
4648
if square:
4749
# don't enlarge image: square size is at most shorter (!) side's length
4850
size_orig = min(img.size)
@@ -52,25 +54,25 @@ def image_thumbnail(image: Image, size: int, square: bool = False) -> Image:
5254
(size_square, size_square),
5355
bleed=0.0,
5456
centering=(0.5, 0.5),
55-
method=Image.BICUBIC,
57+
method=Image.Resampling.BICUBIC,
5658
)
5759
img.thumbnail((size, size))
5860
return img
5961

6062

61-
def image_square(image: Image) -> Image:
63+
def image_square(image: ImageType) -> ImageType:
6264
"""Crop an image to a centered square."""
6365
size = min(image.size)
6466
return ImageOps.fit(
6567
image,
6668
(size, size),
6769
bleed=0.0,
6870
centering=(0.0, 0.5),
69-
method=Image.BICUBIC,
71+
method=Image.Resampling.BICUBIC,
7072
)
7173

7274

73-
def crop_image(image: Image, x1: int, y1: int, x2: int, y2: int) -> Image:
75+
def crop_image(image: ImageType, x1: int, y1: int, x2: int, y2: int) -> ImageType:
7476
"""Crop an image.
7577
7678
The arguments `x1`, `y1`, `x2`, `y2` are the coordinates of the cropped region
@@ -84,7 +86,7 @@ def crop_image(image: Image, x1: int, y1: int, x2: int, y2: int) -> Image:
8486
return image.crop((x1_abs, y1_abs, x2_abs, y2_abs))
8587

8688

87-
def save_image_buffer(image: Image, fmt="JPEG") -> BinaryIO:
89+
def save_image_buffer(image: ImageType, fmt="JPEG") -> BinaryIO:
8890
"""Save an image to a binary buffer."""
8991
buffer = io.BytesIO()
9092
if image.mode != "RGB":
@@ -118,7 +120,7 @@ def __init__(self, stream: BinaryIO, mime_type: str) -> None:
118120
self.is_image = False
119121
self.is_video = False
120122

121-
def get_image(self) -> Image:
123+
def get_image(self) -> ImageType:
122124
"""Get a Pillow Image instance."""
123125
if self.mime_type == MIME_PDF:
124126
return self._get_image_pdf()
@@ -142,7 +144,7 @@ def get_cropped(
142144
img = image_square(img)
143145
return save_image_buffer(img)
144146

145-
def _get_image_pdf(self) -> Image:
147+
def _get_image_pdf(self) -> ImageType:
146148
"""Get a Pillow Image instance of the PDF's first page."""
147149
ims = self._apply_to_path(
148150
convert_from_path, single_file=True, use_cropbox=True, dpi=100
@@ -165,7 +167,7 @@ def _apply_to_path(self, func: Callable, *args, **kwargs):
165167
os.remove(temp_filename)
166168
return output
167169

168-
def _get_image_video(self) -> Image:
170+
def _get_image_video(self) -> ImageType:
169171
"""Get a Pillow Image instance of the video's first frame."""
170172
out, _ = self._apply_to_path(
171173
lambda path: (
@@ -248,7 +250,7 @@ def detect_faces(stream: BinaryIO) -> list[tuple[float, float, float, float]]:
248250
# Extract and normalize face bounding boxes
249251
detected_faces = []
250252
for face in faces[1]:
251-
x, y, w, h = face[:4]
253+
x, y, w, h = np.asarray(face)[:4]
252254
x = float(x)
253255
y = float(y)
254256
w = float(w)

gramps_webapi/api/llm/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from flask import current_app
66
from openai import OpenAI, RateLimitError, APIError
77

8-
from ..search import get_search_indexer
8+
from ..search import get_semantic_search_indexer
99
from ..util import abort_with_message, get_logger
1010

1111

@@ -50,10 +50,11 @@ def answer_prompt(prompt: str, system_prompt: str, config: dict | None = None) -
5050

5151
client = get_client(config=config)
5252
model = config.get("LLM_MODEL")
53+
assert model is not None, "No LLM model specified" # mypy; shouldn't happen
5354

5455
try:
5556
response = client.chat.completions.create(
56-
messages=messages,
57+
messages=messages, # type: ignore
5758
model=model,
5859
)
5960
except RateLimitError:
@@ -64,9 +65,10 @@ def answer_prompt(prompt: str, system_prompt: str, config: dict | None = None) -
6465
abort_with_message(500, "Unexpected error.")
6566

6667
try:
67-
answer = response.to_dict()["choices"][0]["message"]["content"]
68+
answer = response.to_dict()["choices"][0]["message"]["content"] # type: ignore
6869
except (KeyError, IndexError):
6970
abort_with_message(500, "Error parsing chat API response.")
71+
raise # mypy; unreachable
7072

7173
return sanitize_answer(answer)
7274

@@ -102,7 +104,7 @@ def contextualize_prompt(prompt: str, context: str) -> str:
102104

103105

104106
def retrieve(tree: str, prompt: str, include_private: bool, num_results: int = 10):
105-
searcher = get_search_indexer(tree, semantic=True)
107+
searcher = get_semantic_search_indexer(tree)
106108
total, hits = searcher.search(
107109
query=prompt,
108110
page=1,

gramps_webapi/api/media.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
get_db_handle,
4242
get_db_outside_request,
4343
get_tree_from_jwt,
44+
get_tree_from_jwt_or_fail,
4445
)
4546

4647
PREFIX_S3 = "s3://"
@@ -351,9 +352,10 @@ def update_usage_media(
351352
) -> int:
352353
"""Update the usage of media."""
353354
if not tree:
354-
tree = get_tree_from_jwt()
355+
tree = get_tree_from_jwt_or_fail()
355356
if not user_id:
356357
user_id = get_jwt_identity()
358+
assert user_id is not None, "Unexpected error while looking up user ID."
357359
db_handle = get_db_outside_request(
358360
tree=tree, view_private=True, readonly=True, user_id=user_id
359361
)
@@ -371,11 +373,12 @@ def check_quota_media(
371373
) -> None:
372374
"""Check whether the quota allows adding `to_add` bytes and abort if not."""
373375
if not tree:
374-
tree = get_tree_from_jwt()
376+
tree = get_tree_from_jwt_or_fail()
375377
usage_dict = get_tree_usage(tree)
376378
if not usage_dict or usage_dict.get("usage_media") is None:
377379
update_usage_media(tree=tree, user_id=user_id)
378-
usage_dict = get_tree_usage(tree)
380+
usage_dict = get_tree_usage(tree)
381+
assert usage_dict is not None, "Unexpected error while looking up usage data."
379382
usage = usage_dict["usage_media"]
380383
quota = usage_dict.get("quota_media")
381384
if quota is None:

gramps_webapi/api/media_importer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def _identify_missing_files(self) -> MissingFiles:
7777
obj for obj in self.objects if obj.handle not in handles_existing
7878
]
7979

80-
missing_files = {}
80+
missing_files: dict[str, list[dict[str, str]]] = {}
8181
for obj in objects_missing:
8282
if obj.checksum not in missing_files:
8383
missing_files[obj.checksum] = []

0 commit comments

Comments
 (0)