From 869074b22e2454e69a2da62a145708225b8f5dbc Mon Sep 17 00:00:00 2001 From: object-Object Date: Wed, 27 Nov 2024 20:47:48 -0500 Subject: [PATCH] Update Pyright and Pillow, fix many type errors --- CHANGELOG.md | 8 ++++++ pyproject.toml | 5 ++-- src/hexdoc/core/properties/properties.py | 2 +- src/hexdoc/core/resource_dir.py | 10 +++---- src/hexdoc/graphics/block.py | 14 +++++---- src/hexdoc/graphics/camera.py | 2 +- src/hexdoc/graphics/lookups.py | 36 ++++++++++++------------ src/hexdoc/graphics/model/element.py | 21 +++++++++----- src/hexdoc/graphics/texture.py | 3 +- src/hexdoc/model/base.py | 8 +++++- src/hexdoc/model/strip_hidden.py | 5 ++-- src/hexdoc/model/tagged_union.py | 13 +++++---- src/hexdoc/patchouli/book.py | 5 ++-- src/hexdoc/patchouli/text.py | 8 ++++-- src/hexdoc/utils/classproperties.py | 2 +- src/hexdoc/utils/types.py | 29 +++++++++++++++++-- 16 files changed, 114 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 700c9fdf..1efa019b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Pydantic's HISTORY.md](https://github.com/pydantic/pydantic/blob/main/HISTORY.md), and this project *mostly* adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [UNRELEASED] + +### Changed + +* Update dependencies: + * Pyright: `1.1.389` + * Pillow: `11.0.0` + ## `1!0.1.0a20` ### Added diff --git a/pyproject.toml b/pyproject.toml index ddc6102c..9c0f1b48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dependencies = [ "minecraft-render~=2.0.0a6", # Favicons deps, because it's vendored - "pillow~=9.5.0", + "pillow~=11.0.0", "rich~=13.3.4", "svglib~=1.5.1", ] @@ -72,7 +72,7 @@ pdoc = [ "pdoc~=14.1", ] test = [ - "pyright==1.1.361", + "pyright==1.1.389", "pytest~=7.4", "pytest-dependency~=0.5", "pytest-describe~=2.2", @@ -203,7 +203,6 @@ exclude = [ typeCheckingMode = "basic" -enableExperimentalFeatures = true strictDictionaryInference = true strictListInference = true strictSetInference = true diff --git a/src/hexdoc/core/properties/properties.py b/src/hexdoc/core/properties/properties.py index f021c3a2..471baf4c 100644 --- a/src/hexdoc/core/properties/properties.py +++ b/src/hexdoc/core/properties/properties.py @@ -110,7 +110,7 @@ class Properties(BaseProperties): lang: defaultdict[ str, Annotated[LangProps, Field(default_factory=lambda: LangProps())], - ] = Field(default_factory=lambda: defaultdict(LangProps)) + ] = Field(default_factory=lambda: defaultdict[str, LangProps](LangProps)) """Per-language configuration. The key should be the language code, eg. `en_us`.""" extra: dict[str, Any] = Field(default_factory=dict) diff --git a/src/hexdoc/core/resource_dir.py b/src/hexdoc/core/resource_dir.py index 5dd31360..25818cc4 100644 --- a/src/hexdoc/core/resource_dir.py +++ b/src/hexdoc/core/resource_dir.py @@ -18,8 +18,8 @@ from hexdoc.model import HexdocModel from hexdoc.model.base import DEFAULT_CONFIG from hexdoc.plugin import PluginManager -from hexdoc.utils import JSONDict, RelativePath -from hexdoc.utils.types import cast_nullable +from hexdoc.utils import RelativePath +from hexdoc.utils.types import cast_nullable, isdict class BaseResourceDir(HexdocModel, ABC): @@ -65,8 +65,8 @@ def internal(self): @model_validator(mode="before") @classmethod - def _default_reexport(cls, data: JSONDict | Any): - if not isinstance(data, dict): + def _default_reexport(cls, data: Any) -> Any: + if not isdict(data): return data external = cls._get_external(data) @@ -79,7 +79,7 @@ def _default_reexport(cls, data: JSONDict | Any): return data @classmethod - def _get_external(cls, data: JSONDict | Any): + def _get_external(cls, data: dict[Any, Any]): match data: case {"external": bool(), "internal": bool()}: raise ValueError(f"Expected internal OR external, got both: {data}") diff --git a/src/hexdoc/graphics/block.py b/src/hexdoc/graphics/block.py index eddac1b2..6f02073a 100644 --- a/src/hexdoc/graphics/block.py +++ b/src/hexdoc/graphics/block.py @@ -62,7 +62,7 @@ def __init__(self, ctx: Context, wnd: HeadlessWindow): dtype="f4", ) * Matrix44.from_scale((1, -1, 1), "f4") - self.camera, self.eye = direction_camera(pos="south") + self.camera, self.eye = direction_camera(pos=FaceName.south) self.lights = [ ((0, -1, 0), LIGHT_TOP), @@ -100,9 +100,9 @@ def __init__(self, ctx: Context, wnd: HeadlessWindow): pos = 8 neg = 0 for from_, to, color, direction in [ - ((0, neg, neg), (0, pos, pos), (1, 0, 0, 0.75), "east"), - ((neg, 0, neg), (pos, 0, pos), (0, 1, 0, 0.75), "up"), - ((neg, neg, 0), (pos, pos, 0), (0, 0, 1, 0.75), "south"), + ((0, neg, neg), (0, pos, pos), (1, 0, 0, 0.75), FaceName.east), + ((neg, 0, neg), (pos, 0, pos), (0, 1, 0, 0.75), FaceName.up), + ((neg, neg, 0), (pos, pos, 0), (0, 0, 1, 0.75), FaceName.south), ]: vao = VAO() verts = get_face_verts(from_, to, direction) @@ -153,7 +153,9 @@ def render_block( frame_height = texture.frame_height layers = len(texture.frames) - min_alpha, _ = cast(tuple[int, int], image.getextrema()[3]) + extrema = image.getextrema() + assert len(extrema) >= 4, f"Expected 4 bands but got {len(extrema)}" + min_alpha, _ = extrema[3] if min_alpha < 255: logger.debug(f"Transparent texture: {name} ({min_alpha=})") transparent_textures.add(name) @@ -256,7 +258,7 @@ def _render_frame(self, baked_faces: list[BakedFace], debug: DebugType, tick: in mode="RGBA", size=self.wnd.fbo.size, data=self.wnd.fbo.read(components=4), - ).transpose(Image.FLIP_TOP_BOTTOM) + ).transpose(Image.Transpose.FLIP_TOP_BOTTOM) return image diff --git a/src/hexdoc/graphics/camera.py b/src/hexdoc/graphics/camera.py index 08018c45..4ccea7ac 100644 --- a/src/hexdoc/graphics/camera.py +++ b/src/hexdoc/graphics/camera.py @@ -43,7 +43,7 @@ def orbit_camera(pitch: float, yaw: float): ), eye -def direction_camera(pos: FaceName, up: FaceName = "up"): +def direction_camera(pos: FaceName, up: FaceName = FaceName.up): """eg. north -> camera is placed to the north of the model, looking south""" eye = get_direction_vec(pos, 64) return Matrix44.look_at( diff --git a/src/hexdoc/graphics/lookups.py b/src/hexdoc/graphics/lookups.py index 8028d184..5da9495f 100644 --- a/src/hexdoc/graphics/lookups.py +++ b/src/hexdoc/graphics/lookups.py @@ -13,7 +13,7 @@ def get_face_verts(from_: Vec3, to: Vec3, direction: FaceName): # fmt: off match direction: - case "south": + case FaceName.south: return [ x2, y1, z2, x2, y2, z2, @@ -22,7 +22,7 @@ def get_face_verts(from_: Vec3, to: Vec3, direction: FaceName): x1, y2, z2, x1, y1, z2, ] - case "east": + case FaceName.east: return [ x2, y1, z1, x2, y2, z1, @@ -31,7 +31,7 @@ def get_face_verts(from_: Vec3, to: Vec3, direction: FaceName): x2, y2, z2, x2, y1, z2, ] - case "down": + case FaceName.down: return [ x2, y1, z1, x2, y1, z2, @@ -40,7 +40,7 @@ def get_face_verts(from_: Vec3, to: Vec3, direction: FaceName): x1, y1, z2, x1, y1, z1, ] - case "west": + case FaceName.west: return [ x1, y1, z2, x1, y2, z2, @@ -49,7 +49,7 @@ def get_face_verts(from_: Vec3, to: Vec3, direction: FaceName): x1, y2, z1, x1, y1, z1, ] - case "north": + case FaceName.north: return [ x2, y2, z1, x2, y1, z1, @@ -58,7 +58,7 @@ def get_face_verts(from_: Vec3, to: Vec3, direction: FaceName): x1, y1, z1, x1, y2, z1, ] - case "up": + case FaceName.up: return [ x2, y2, z1, x1, y2, z1, @@ -72,33 +72,33 @@ def get_face_verts(from_: Vec3, to: Vec3, direction: FaceName): def get_face_uv_indices(direction: FaceName): match direction: - case "south": + case FaceName.south: return (2, 3, 1, 3, 0, 1) - case "east": + case FaceName.east: return (2, 3, 1, 3, 0, 1) - case "down": + case FaceName.down: return (2, 3, 0, 2, 0, 1) - case "west": + case FaceName.west: return (2, 3, 0, 2, 0, 1) - case "north": + case FaceName.north: return (0, 1, 2, 0, 2, 3) - case "up": + case FaceName.up: return (3, 0, 2, 0, 1, 2) def get_direction_vec(direction: FaceName, magnitude: float = 1): match direction: - case "north": + case FaceName.north: return (0, 0, -magnitude) - case "south": + case FaceName.south: return (0, 0, magnitude) - case "west": + case FaceName.west: return (-magnitude, 0, 0) - case "east": + case FaceName.east: return (magnitude, 0, 0) - case "down": + case FaceName.down: return (0, -magnitude, 0) - case "up": + case FaceName.up: return (0, magnitude, 0) diff --git a/src/hexdoc/graphics/model/element.py b/src/hexdoc/graphics/model/element.py index 6f7f5e3e..384920d2 100644 --- a/src/hexdoc/graphics/model/element.py +++ b/src/hexdoc/graphics/model/element.py @@ -3,6 +3,7 @@ import logging import math import re +from enum import StrEnum from typing import Annotated, Literal from pydantic import AfterValidator, Field @@ -73,7 +74,13 @@ def eulers(self) -> Vec3: return (0, 0, angle) -FaceName = Literal["down", "up", "north", "south", "west", "east"] +class FaceName(StrEnum): + down = "down" + up = "up" + north = "north" + south = "south" + west = "west" + east = "east" class ElementFace(HexdocModel): @@ -148,17 +155,17 @@ def default(cls, element: Element, direction: FaceName): uvs: Vec4 match direction: - case "down": + case FaceName.down: uvs = (x1, 16 - z2, x2, 16 - z1) - case "up": + case FaceName.up: uvs = (x1, z1, x2, z2) - case "north": + case FaceName.north: uvs = (16 - x2, 16 - y2, 16 - x1, 16 - y1) - case "south": + case FaceName.south: uvs = (x1, 16 - y2, x2, 16 - y1) - case "west": + case FaceName.west: uvs = (z1, 16 - y2, z2, 16 - y1) - case "east": + case FaceName.east: uvs = (16 - z2, 16 - y2, 16 - z1, 16 - y1) return cls(uvs=uvs) diff --git a/src/hexdoc/graphics/texture.py b/src/hexdoc/graphics/texture.py index c82c4975..cf0d36ac 100644 --- a/src/hexdoc/graphics/texture.py +++ b/src/hexdoc/graphics/texture.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from functools import cached_property from pathlib import Path +from typing import Iterator import numpy as np from PIL import Image @@ -105,7 +106,7 @@ def get_frame(self, tick: int): @cached_property @listify - def frames(self): + def frames(self) -> Iterator[Image.Image]: """Returns a list of animation frames, where each frame lasts for one tick. If `animation` is None, just returns `[image]`. diff --git a/src/hexdoc/model/base.py b/src/hexdoc/model/base.py index 45eed580..8ba50833 100644 --- a/src/hexdoc/model/base.py +++ b/src/hexdoc/model/base.py @@ -120,7 +120,13 @@ def _hexdoc_check_model_field_type( else: annotations = [] - if any(isinstance(a, SkipValidation) for a in annotations): + if any( + isinstance( + a, + SkipValidation, # pyright: ignore[reportArgumentType] + ) + for a in annotations + ): return origin_stack.append(origin) diff --git a/src/hexdoc/model/strip_hidden.py b/src/hexdoc/model/strip_hidden.py index fe271027..38c14591 100644 --- a/src/hexdoc/model/strip_hidden.py +++ b/src/hexdoc/model/strip_hidden.py @@ -4,6 +4,7 @@ from pydantic.config import JsonDict from hexdoc.utils.deserialize.assertions import cast_or_raise +from hexdoc.utils.types import isdict from .base import DEFAULT_CONFIG, HexdocModel @@ -25,8 +26,8 @@ class StripHiddenModel(HexdocModel): ) @model_validator(mode="before") - def _pre_root_strip_hidden(cls, values: dict[Any, Any] | Any) -> Any: - if not isinstance(values, dict): + def _pre_root_strip_hidden(cls, values: Any) -> Any: + if not isdict(values): return values return { diff --git a/src/hexdoc/model/tagged_union.py b/src/hexdoc/model/tagged_union.py index f6ebca40..b20d11e6 100644 --- a/src/hexdoc/model/tagged_union.py +++ b/src/hexdoc/model/tagged_union.py @@ -11,6 +11,7 @@ Iterable, LiteralString, Self, + TypeVar, Unpack, ) @@ -40,6 +41,8 @@ from .base import HexdocModel +_T_UnionModel = TypeVar("_T_UnionModel", bound="UnionModel") + TagValue = str | NoValueType _RESOLVED = "__resolved" @@ -55,15 +58,15 @@ def _resolve_union( value: Any, context: dict[str, Any] | None, *, - model_types: Iterable[type[Self]], + model_types: Iterable[type[_T_UnionModel]], allow_ambiguous: bool, error_name: LiteralString = "HexdocUnionMatchError", error_text: Iterable[LiteralString] = [], error_data: dict[str, Any] = {}, - ) -> Self: + ) -> _T_UnionModel: # try all the types exceptions: list[InitErrorDetails] = [] - matches: dict[type[Self], Self] = {} + matches: dict[type[_T_UnionModel], _T_UnionModel] = {} for model_type in model_types: try: @@ -271,14 +274,14 @@ def _resolve_from_dict( raise @model_validator(mode="before") - def _pop_temporary_keys(cls, value: dict[Any, Any] | Any): + def _pop_temporary_keys(cls, value: Any) -> Any: if isinstance(value, dict) and _RESOLVED in value: # copy because this validator may be called multiple times # eg. two types with the same key value = value.copy() value.pop(_RESOLVED) assert value.pop(cls._tag_key, NoValue) == cls._tag_value - return value + return value # pyright: ignore[reportUnknownVariableType] @classmethod @override diff --git a/src/hexdoc/patchouli/book.py b/src/hexdoc/patchouli/book.py index 5a373434..236c194c 100644 --- a/src/hexdoc/patchouli/book.py +++ b/src/hexdoc/patchouli/book.py @@ -13,6 +13,7 @@ from hexdoc.core.compat import AtLeast_1_20, Before_1_20 from hexdoc.model import Color, HexdocModel from hexdoc.utils import ContextSource, cast_context, sorted_dict +from hexdoc.utils.types import isdict from .book_context import BookContext from .category import Category @@ -147,8 +148,8 @@ def _load_entries( @model_validator(mode="before") @classmethod - def _pre_root(cls, data: dict[Any, Any] | Any): - if isinstance(data, dict) and "index_icon" not in data: + def _pre_root(cls, data: Any): + if isdict(data) and "index_icon" not in data: data["index_icon"] = data.get("model") return data diff --git a/src/hexdoc/patchouli/text.py b/src/hexdoc/patchouli/text.py index c7d7c7fd..06905ef0 100644 --- a/src/hexdoc/patchouli/text.py +++ b/src/hexdoc/patchouli/text.py @@ -6,7 +6,7 @@ import re from enum import Enum, auto from fnmatch import fnmatch -from typing import Literal, Self, final +from typing import Literal, Self, TypedDict, final from jinja2 import pass_context from jinja2.runtime import Context @@ -285,6 +285,10 @@ class FunctionStyle(Style, frozen=True): value: str +class BookLinksDict(TypedDict): + book_links: BookLinks + + class LinkStyle(Style, frozen=True): type: Literal[SpecialStyleType.link] = SpecialStyleType.link value: str | BookLink @@ -313,7 +317,7 @@ def from_str( return cls(value=value, external=external) @pass_context - def href(self, context: Context | dict[{"book_links": BookLinks}]): # noqa + def href(self, context: Context | BookLinksDict): match self.value: case str(href): return href diff --git a/src/hexdoc/utils/classproperties.py b/src/hexdoc/utils/classproperties.py index e86d8c20..083e0a14 100644 --- a/src/hexdoc/utils/classproperties.py +++ b/src/hexdoc/utils/classproperties.py @@ -23,6 +23,6 @@ def __get__(self, _: Any, cls: type[_T_cv]) -> _R_co: def classproperty( func: Callable[[type[_T_cv]], _R_co], ) -> ClassPropertyDescriptor[_T_cv, _R_co]: - if isinstance(func, classmethod): + if isinstance(func, classmethod): # pyright: ignore[reportUnnecessaryIsInstance] return ClassPropertyDescriptor(func) return ClassPropertyDescriptor(classmethod(func)) diff --git a/src/hexdoc/utils/types.py b/src/hexdoc/utils/types.py index d4596a8c..f45f5db4 100644 --- a/src/hexdoc/utils/types.py +++ b/src/hexdoc/utils/types.py @@ -1,7 +1,17 @@ import functools from abc import ABC, abstractmethod from enum import Enum, unique -from typing import Annotated, Any, Callable, Mapping, ParamSpec, Protocol, get_args +from typing import ( + Annotated, + Any, + Callable, + Mapping, + ParamSpec, + Protocol, + TypeGuard, + get_args, + overload, +) from ordered_set import OrderedSet, OrderedSetInitializer from pydantic import ( @@ -21,6 +31,8 @@ _T_float = TypeVar("_T_float", default=float) +_T_dict = TypeVar("_T_dict", bound=dict[Any, Any]) + Vec2 = tuple[_T_float, _T_float] Vec3 = tuple[_T_float, _T_float, _T_float] @@ -175,7 +187,7 @@ def typed_partial(f: Callable[_P, _R]) -> Callable[_P, Callable[_P, _R]]: def builder_builder(*partial_args: _P.args, **partial_kwargs: _P.kwargs): @functools.wraps(f) def builder(*args: _P.args, **kwargs: _P.kwargs): - return f(*partial_args, *args, **partial_kwargs, **kwargs) + return f(*partial_args, *args, **partial_kwargs, **kwargs) # type: ignore return builder @@ -188,3 +200,16 @@ def cast_nullable(value: _T) -> _T | None: At runtime, just returns the value as-is. """ return value + + +@overload +def isdict(value: _T_dict) -> TypeGuard[_T_dict]: ... + + +@overload +def isdict(value: Any) -> TypeGuard[dict[Any, Any]]: ... + + +def isdict(value: Any) -> TypeGuard[dict[Any, Any]]: + """As `isinstance(value, dict)`, but narrows to `dict[Any, Any]` instead of unknown.""" + return isinstance(value, dict)