From 745fea6b85389379e05f1cd4f09fdaf223194575 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:52:18 -0700 Subject: [PATCH] refactor: addresss type hints in query_lang --- src/tagstudio/core/query_lang/ast.py | 13 ++++++++--- src/tagstudio/core/query_lang/parser.py | 27 ++++++++++++++++------ src/tagstudio/core/query_lang/tokenizer.py | 15 +++++++++--- src/tagstudio/core/query_lang/util.py | 11 +++++++++ 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/tagstudio/core/query_lang/ast.py b/src/tagstudio/core/query_lang/ast.py index 102203ed..0323bf26 100644 --- a/src/tagstudio/core/query_lang/ast.py +++ b/src/tagstudio/core/query_lang/ast.py @@ -1,6 +1,11 @@ +# Copyright (C) 2025 +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + from abc import ABC, abstractmethod from enum import Enum -from typing import Generic, TypeVar, Union +from typing import Generic, TypeVar, override class ConstraintType(Enum): @@ -12,7 +17,7 @@ class ConstraintType(Enum): Special = 5 @staticmethod - def from_string(text: str) -> Union["ConstraintType", None]: + def from_string(text: str) -> "ConstraintType | None": return { "tag": ConstraintType.Tag, "tag_id": ConstraintType.TagID, @@ -24,14 +29,16 @@ class ConstraintType(Enum): class AST: - parent: Union["AST", None] = None + parent: "AST | None" = None + @override def __str__(self): class_name = self.__class__.__name__ fields = vars(self) # Get all instance variables as a dictionary field_str = ", ".join(f"{key}={value}" for key, value in fields.items()) return f"{class_name}({field_str})" + @override def __repr__(self) -> str: return self.__str__() diff --git a/src/tagstudio/core/query_lang/parser.py b/src/tagstudio/core/query_lang/parser.py index 8566d5db..ff17465d 100644 --- a/src/tagstudio/core/query_lang/parser.py +++ b/src/tagstudio/core/query_lang/parser.py @@ -1,3 +1,8 @@ +# Copyright (C) 2025 +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + from tagstudio.core.query_lang.ast import ( AST, ANDList, @@ -27,7 +32,7 @@ class Parser: if self.next_token.type == TokenType.EOF: return ORList([]) out = self.__or_list() - if self.next_token.type != TokenType.EOF: + if self.next_token.type != TokenType.EOF: # pyright: ignore[reportUnnecessaryComparison] raise ParsingError(self.next_token.start, self.next_token.end, "Syntax Error") return out @@ -41,7 +46,7 @@ class Parser: return ORList(terms) if len(terms) > 1 else terms[0] def __is_next_or(self) -> bool: - return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "OR" + return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "OR" # pyright: ignore def __and_list(self) -> AST: elements = [self.__term()] @@ -67,7 +72,7 @@ class Parser: raise self.__syntax_error("Unexpected AND") def __is_next_and(self) -> bool: - return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "AND" + return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "AND" # pyright: ignore def __term(self) -> AST: if self.__is_next_not(): @@ -85,11 +90,14 @@ class Parser: return self.__constraint() def __is_next_not(self) -> bool: - return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "NOT" + return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "NOT" # pyright: ignore def __constraint(self) -> Constraint: if self.next_token.type == TokenType.CONSTRAINTTYPE: - self.last_constraint_type = self.__eat(TokenType.CONSTRAINTTYPE).value + constraint = self.__eat(TokenType.CONSTRAINTTYPE).value + if not isinstance(constraint, ConstraintType): + raise self.__syntax_error() + self.last_constraint_type = constraint value = self.__literal() @@ -98,7 +106,7 @@ class Parser: self.__eat(TokenType.SBRACKETO) properties.append(self.__property()) - while self.next_token.type == TokenType.COMMA: + while self.next_token.type == TokenType.COMMA: # pyright: ignore[reportUnnecessaryComparison] self.__eat(TokenType.COMMA) properties.append(self.__property()) @@ -110,11 +118,16 @@ class Parser: key = self.__eat(TokenType.ULITERAL).value self.__eat(TokenType.EQUALS) value = self.__literal() + if not isinstance(key, str): + raise self.__syntax_error() return Property(key, value) def __literal(self) -> str: if self.next_token.type in [TokenType.QLITERAL, TokenType.ULITERAL]: - return self.__eat(self.next_token.type).value + literal = self.__eat(self.next_token.type).value + if not isinstance(literal, str): + raise self.__syntax_error() + return literal raise self.__syntax_error() def __eat(self, type: TokenType) -> Token: diff --git a/src/tagstudio/core/query_lang/tokenizer.py b/src/tagstudio/core/query_lang/tokenizer.py index 4970a5fe..a279fbf6 100644 --- a/src/tagstudio/core/query_lang/tokenizer.py +++ b/src/tagstudio/core/query_lang/tokenizer.py @@ -1,5 +1,10 @@ +# Copyright (C) 2025 +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + from enum import Enum -from typing import Any +from typing import override from tagstudio.core.query_lang.ast import ConstraintType from tagstudio.core.query_lang.util import ParsingError @@ -21,12 +26,14 @@ class TokenType(Enum): class Token: type: TokenType - value: Any + value: str | ConstraintType | None start: int end: int - def __init__(self, type: TokenType, value: Any, start: int, end: int) -> None: + def __init__( + self, type: TokenType, value: str | ConstraintType | None, start: int, end: int + ) -> None: self.type = type self.value = value self.start = start @@ -40,9 +47,11 @@ class Token: def EOF(pos: int) -> "Token": # noqa: N802 return Token.from_type(TokenType.EOF, pos) + @override def __str__(self) -> str: return f"Token({self.type}, {self.value}, {self.start}, {self.end})" # pragma: nocover + @override def __repr__(self) -> str: return self.__str__() # pragma: nocover diff --git a/src/tagstudio/core/query_lang/util.py b/src/tagstudio/core/query_lang/util.py index 8deaecf2..95e53dbe 100644 --- a/src/tagstudio/core/query_lang/util.py +++ b/src/tagstudio/core/query_lang/util.py @@ -1,15 +1,26 @@ +# Copyright (C) 2025 +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from typing import override + + class ParsingError(BaseException): start: int end: int msg: str def __init__(self, start: int, end: int, msg: str = "Syntax Error") -> None: + super().__init__() self.start = start self.end = end self.msg = msg + @override def __str__(self) -> str: return f"Syntax Error {self.start}->{self.end}: {self.msg}" # pragma: nocover + @override def __repr__(self) -> str: return self.__str__() # pragma: nocover