From 7bf2d0d8fc8ab653bdfc90f12f8197bf99aa334f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20H=C3=A4m=C3=A4l=C3=A4inen?= Date: Sat, 29 Aug 2026 21:10:40 +0300 Subject: [PATCH] Split Map2d.iter_data overloads by direction literal The iter_data overloads came in pairs per arity: one keyed on Literal[IterDirection.Rows] returning row-major tuples, and a general one accepting any IterDirection and returning column-major tuples, the general signature declared first. Overload resolution picks the first match, and Literal[IterDirection.Rows] is assignable to IterDirection, so the general signature swallowed every call that named IterDirection.Rows explicitly and typed its result as column-major - the transpose of what the implementation yields. Merely declaring the literal signature first moves that unsoundness rather than removing it: the general signature then still promises column-major for an argument that may be Rows at run time, which mypy reports as an overload-overlap error. Give each arity three signatures instead: Literal[IterDirection.Rows], Literal[IterDirection.Columns], and a general IterDirection returning the union of both shapes. The two literal signatures are disjoint, and each returns a subtype of the union the general signature promises, so no unsafe overlap is left. The union is also the honest answer for a caller whose direction is only known as an IterDirection, since iter_data branches on that value at run time. Behaviour is unchanged and no call site needed adjusting. --- solvers/python/src/aoc/tooling/map.py | 39 ++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/solvers/python/src/aoc/tooling/map.py b/solvers/python/src/aoc/tooling/map.py index e2784ab..906c0f2 100644 --- a/solvers/python/src/aoc/tooling/map.py +++ b/solvers/python/src/aoc/tooling/map.py @@ -163,21 +163,45 @@ def iter_data_by_columns( @overload def iter_data( - self, *, direction: IterDirection + self, *, direction: Literal[IterDirection.Rows] = IterDirection.Rows + ) -> Iterable[tuple[Y, Iterable[tuple[X, Map2dDataType]]]]: ... + @overload + def iter_data( + self, *, direction: Literal[IterDirection.Columns] ) -> Iterable[tuple[X, Iterable[tuple[Y, Map2dDataType]]]]: ... @overload def iter_data( - self, *, direction: Literal[IterDirection.Rows] = IterDirection.Rows + self, *, direction: IterDirection + ) -> ( + Iterable[tuple[Y, Iterable[tuple[X, Map2dDataType]]]] + | Iterable[tuple[X, Iterable[tuple[Y, Map2dDataType]]]] + ): ... + @overload + def iter_data( + self, + first_y: Y, + first_x: X, + *, + direction: Literal[IterDirection.Rows] = IterDirection.Rows, ) -> Iterable[tuple[Y, Iterable[tuple[X, Map2dDataType]]]]: ... @overload def iter_data( - self, first_y: Y, first_x: X, *, direction: IterDirection + self, first_y: Y, first_x: X, *, direction: Literal[IterDirection.Columns] ) -> Iterable[tuple[X, Iterable[tuple[Y, Map2dDataType]]]]: ... @overload + def iter_data( + self, first_y: Y, first_x: X, *, direction: IterDirection + ) -> ( + Iterable[tuple[Y, Iterable[tuple[X, Map2dDataType]]]] + | Iterable[tuple[X, Iterable[tuple[Y, Map2dDataType]]]] + ): ... + @overload def iter_data( self, first_y: Y, first_x: X, + last_y: Y, + last_x: X, *, direction: Literal[IterDirection.Rows] = IterDirection.Rows, ) -> Iterable[tuple[Y, Iterable[tuple[X, Map2dDataType]]]]: ... @@ -189,7 +213,7 @@ def iter_data( last_y: Y, last_x: X, *, - direction: IterDirection, + direction: Literal[IterDirection.Columns], ) -> Iterable[tuple[X, Iterable[tuple[Y, Map2dDataType]]]]: ... @overload def iter_data( @@ -199,8 +223,11 @@ def iter_data( last_y: Y, last_x: X, *, - direction: Literal[IterDirection.Rows] = IterDirection.Rows, - ) -> Iterable[tuple[Y, Iterable[tuple[X, Map2dDataType]]]]: ... + direction: IterDirection, + ) -> ( + Iterable[tuple[Y, Iterable[tuple[X, Map2dDataType]]]] + | Iterable[tuple[X, Iterable[tuple[Y, Map2dDataType]]]] + ): ... def iter_data( self,