From 630770fce940e0745bd94ecd777b84e234891939 Mon Sep 17 00:00:00 2001 From: Henry Lee Date: Tue, 12 May 2026 13:34:09 +0000 Subject: [PATCH 1/5] feat(types): enable pyright strict mode and fix all type errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add [tool.pyright] typeCheckingMode = "strict" to pyproject.toml - Replace Any with object in all __eq__/__ne__ signatures (StartVariable, Item, State, Rule) - Extract State._items_hash property to expose a non-generic int for equality comparison, avoiding Unknown member access after isinstance narrowing - Fix Grammar type alias to use type[object] | None instead of bare type | None so Parser[T] inference resolves T = object - Annotate grammar dict in test_prec_override with full union type to match Parser.__init__ signature - Fix right: list → list[type[Token] | str] in test_hash_semantics - Remove unused pytest import from test_prec_override Co-Authored-By: Claude Sonnet 4.6 --- plare/parser.py | 20 ++++++++++++-------- pyproject.toml | 3 +++ tests/test_determinism.py | 4 ++-- tests/test_hash_semantics.py | 2 +- tests/test_prec_override.py | 12 +++++++++--- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/plare/parser.py b/plare/parser.py index e2ca97f..f80a36a 100644 --- a/plare/parser.py +++ b/plare/parser.py @@ -22,7 +22,7 @@ from collections import deque from itertools import chain -from typing import Any, Iterable, Protocol +from typing import Iterable, Protocol from plare.exception import ParserError, ParsingError from plare.token import Token @@ -106,10 +106,10 @@ def __init__(self, variable: str) -> None: def __hash__(self) -> int: return super().__hash__() - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: return isinstance(other, StartVariable) and super().__eq__(other) - def __ne__(self, other: Any) -> bool: + def __ne__(self, other: object) -> bool: return not isinstance(other, StartVariable) or super().__ne__(other) @@ -211,7 +211,7 @@ def __str__(self) -> str: def __hash__(self) -> int: return hash((self.left, tuple(self.right), self.loc)) - def __eq__(self, value: Any) -> bool: + def __eq__(self, value: object) -> bool: return ( isinstance(value, Item) and self.left == value.left @@ -241,11 +241,15 @@ def __init__(self, id: int, items: set[Item[T]]) -> None: self.items = items self.lookaheads = {} - def __hash__(self) -> int: + @property + def _items_hash(self) -> int: return hash(frozenset(self.items)) - def __eq__(self, other: State[T] | Any) -> bool: - return isinstance(other, State) and self.items == other.items + def __hash__(self) -> int: + return self._items_hash + + def __eq__(self, other: object) -> bool: + return isinstance(other, State) and self._items_hash == other._items_hash def __str__(self) -> str: return "\n".join(map(str, self.items)) @@ -494,7 +498,7 @@ def calc_follow(self, rules: dict[str, Rule[T]]) -> set[type[Token]]: def __hash__(self) -> int: return hash(self.left) - def __eq__(self, value: Any) -> bool: + def __eq__(self, value: object) -> bool: return isinstance(value, Rule) and self.left == value.left def __repr__(self) -> str: diff --git a/pyproject.toml b/pyproject.toml index 1a201e7..592d1aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,5 +53,8 @@ markers = [ "slow: marks tests as slow-running (deselect with '-m \"not slow\"')", ] +[tool.pyright] +typeCheckingMode = "strict" + [tool.isort] profile = "black" diff --git a/tests/test_determinism.py b/tests/test_determinism.py index 7a9461f..9005010 100644 --- a/tests/test_determinism.py +++ b/tests/test_determinism.py @@ -19,8 +19,8 @@ type Grammar = dict[ str, list[ - tuple[list[type[Token] | str], type | None, list[int]] - | tuple[list[type[Token] | str], type | None, list[int], type[Token]] + tuple[list[type[Token] | str], type[object] | None, list[int]] + | tuple[list[type[Token] | str], type[object] | None, list[int], type[Token]] ], ] diff --git a/tests/test_hash_semantics.py b/tests/test_hash_semantics.py index d6b7a79..7c7ba2d 100644 --- a/tests/test_hash_semantics.py +++ b/tests/test_hash_semantics.py @@ -18,7 +18,7 @@ def __init__(self, value: str, *, lineno: int, offset: int) -> None: self.offset = offset -def make_item(left: str, right: list, loc: int = 0) -> Item[object]: +def make_item(left: str, right: list[type[Token] | str], loc: int = 0) -> Item[object]: return Item(left, right, IDMaker(0), 0, loc) diff --git a/tests/test_prec_override.py b/tests/test_prec_override.py index 9cd702f..cc115e8 100644 --- a/tests/test_prec_override.py +++ b/tests/test_prec_override.py @@ -8,8 +8,6 @@ from __future__ import annotations -import pytest - from plare.parser import Parser from plare.token import Token @@ -185,7 +183,15 @@ def test_rr_equal_precedence_first_defined_wins() -> None: ParserError. With it, first_val (defined earlier, lower definition_index) wins, so the reduction always produces FirstResult. """ - grammar: dict = { + grammar: dict[ + str, + list[ + tuple[list[type[Token] | str], type[object] | None, list[int]] + | tuple[ + list[type[Token] | str], type[object] | None, list[int], type[Token] + ] + ], + ] = { "result": [ (["first_val"], FirstResult, [0]), (["second_val"], SecondResult, [0]), From ee4fa47844103dc541cd384d4fdf0602744cf876 Mon Sep 17 00:00:00 2001 From: Henry Lee Date: Tue, 12 May 2026 13:42:25 +0000 Subject: [PATCH 2/5] refactor(types): replace _items_hash workaround with TypeGuard Use TypeGuard[State[object]] in _is_state() so pyright narrows other to State[object] after the guard, making other.items a fully-known set[Item[object]] and allowing the original set comparison to be restored. Co-Authored-By: Claude Sonnet 4.6 --- plare/parser.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plare/parser.py b/plare/parser.py index f80a36a..cc3c79d 100644 --- a/plare/parser.py +++ b/plare/parser.py @@ -22,7 +22,7 @@ from collections import deque from itertools import chain -from typing import Iterable, Protocol +from typing import Iterable, Protocol, TypeGuard from plare.exception import ParserError, ParsingError from plare.token import Token @@ -241,20 +241,20 @@ def __init__(self, id: int, items: set[Item[T]]) -> None: self.items = items self.lookaheads = {} - @property - def _items_hash(self) -> int: - return hash(frozenset(self.items)) - def __hash__(self) -> int: - return self._items_hash + return hash(frozenset(self.items)) def __eq__(self, other: object) -> bool: - return isinstance(other, State) and self._items_hash == other._items_hash + return _is_state(other) and self.items == other.items def __str__(self) -> str: return "\n".join(map(str, self.items)) +def _is_state(obj: object) -> TypeGuard[State[object]]: + return isinstance(obj, State) + + class Shift: """LR table action: shift the lookahead token and push state ``next``.""" From 227bb6ad4a4176b04f425bd55c3b4a5779a8b703 Mon Sep 17 00:00:00 2001 From: Henry Lee Date: Tue, 12 May 2026 13:45:10 +0000 Subject: [PATCH 3/5] style: rename _is_state to is_state Co-Authored-By: Claude Sonnet 4.6 --- plare/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plare/parser.py b/plare/parser.py index cc3c79d..351cc59 100644 --- a/plare/parser.py +++ b/plare/parser.py @@ -245,13 +245,13 @@ def __hash__(self) -> int: return hash(frozenset(self.items)) def __eq__(self, other: object) -> bool: - return _is_state(other) and self.items == other.items + return is_state(other) and self.items == other.items def __str__(self) -> str: return "\n".join(map(str, self.items)) -def _is_state(obj: object) -> TypeGuard[State[object]]: +def is_state(obj: object) -> TypeGuard[State[object]]: return isinstance(obj, State) From 7d8ef6e68482a54d94665c35544d1afe4b393c41 Mon Sep 17 00:00:00 2001 From: Henry Lee Date: Tue, 12 May 2026 13:48:31 +0000 Subject: [PATCH 4/5] refactor(types): use classmethod TypeGuard to preserve State[T] in __eq__ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace module-level is_state() returning TypeGuard[State[object]] with State.is_instance() classmethod returning TypeGuard[State[T]]. When called as self.is_instance(other), cls is bound to type[State[T]], so T flows from the caller's type parameter into the TypeGuard — narrowing other to State[T] instead of State[object]. Co-Authored-By: Claude Sonnet 4.6 --- plare/parser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plare/parser.py b/plare/parser.py index 351cc59..b9a893a 100644 --- a/plare/parser.py +++ b/plare/parser.py @@ -244,17 +244,17 @@ def __init__(self, id: int, items: set[Item[T]]) -> None: def __hash__(self) -> int: return hash(frozenset(self.items)) + @classmethod + def is_instance(cls, obj: object) -> TypeGuard[State[T]]: + return isinstance(obj, State) + def __eq__(self, other: object) -> bool: - return is_state(other) and self.items == other.items + return self.is_instance(other) and self.items == other.items def __str__(self) -> str: return "\n".join(map(str, self.items)) -def is_state(obj: object) -> TypeGuard[State[object]]: - return isinstance(obj, State) - - class Shift: """LR table action: shift the lookahead token and push state ``next``.""" From 5b06ef32d64ee4b3d56d26ce0df9555cb1ab9726 Mon Sep 17 00:00:00 2001 From: Henry Lee Date: Tue, 12 May 2026 13:54:10 +0000 Subject: [PATCH 5/5] fix(types): make is_instance an instance method, not classmethod T is bound through the instance (self: State[T]), not the class alone. Co-Authored-By: Claude Sonnet 4.6 --- plare/parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plare/parser.py b/plare/parser.py index b9a893a..c30bea5 100644 --- a/plare/parser.py +++ b/plare/parser.py @@ -244,8 +244,7 @@ def __init__(self, id: int, items: set[Item[T]]) -> None: def __hash__(self) -> int: return hash(frozenset(self.items)) - @classmethod - def is_instance(cls, obj: object) -> TypeGuard[State[T]]: + def is_instance(self, obj: object) -> TypeGuard[State[T]]: return isinstance(obj, State) def __eq__(self, other: object) -> bool: