Linted and formatted everything new

This commit is contained in:
2026-03-16 02:42:23 +01:00
parent 40ca08359f
commit d427cec8d5
18 changed files with 1410 additions and 1209 deletions

View File

@@ -6,16 +6,15 @@ field filters, and edge case handling.
"""
from datetime import date
from typing import List
import pytest
from hxbooks.search import (
ComparisonOperator,
Field,
FieldFilter,
QueryParser,
SearchQuery,
_convert_value, # noqa: PLC2701
)
@@ -28,31 +27,31 @@ def parser() -> QueryParser:
class TestQueryParser:
"""Test the QueryParser class functionality."""
def test_parse_empty_query(self, parser: QueryParser):
def test_parse_empty_query(self, parser: QueryParser) -> None:
"""Test parsing an empty query string."""
result = parser.parse("")
assert result.text_terms == []
assert result.field_filters == []
def test_parse_whitespace_only(self, parser: QueryParser):
def test_parse_whitespace_only(self, parser: QueryParser) -> None:
"""Test parsing a query with only whitespace."""
result = parser.parse(" \t\n ")
assert result.text_terms == []
assert result.field_filters == []
def test_parse_simple_text_terms(self, parser: QueryParser):
def test_parse_simple_text_terms(self, parser: QueryParser) -> None:
"""Test parsing simple text search terms."""
result = parser.parse("hobbit tolkien")
assert result.text_terms == ["hobbit", "tolkien"]
assert result.field_filters == []
def test_parse_quoted_text_terms(self, parser: QueryParser):
def test_parse_quoted_text_terms(self, parser: QueryParser) -> None:
"""Test parsing quoted text search terms."""
result = parser.parse('"the hobbit" tolkien')
assert result.text_terms == ["the hobbit", "tolkien"]
assert result.field_filters == []
def test_parse_quoted_text_with_spaces(self, parser: QueryParser):
def test_parse_quoted_text_with_spaces(self, parser: QueryParser) -> None:
"""Test parsing quoted text containing multiple spaces."""
result = parser.parse('"lord of the rings"')
assert result.text_terms == ["lord of the rings"]
@@ -62,7 +61,7 @@ class TestQueryParser:
class TestFieldFilters:
"""Test field filter parsing."""
def test_parse_title_filter(self, parser: QueryParser):
def test_parse_title_filter(self, parser: QueryParser) -> None:
"""Test parsing title field filter."""
result = parser.parse("title:hobbit")
assert len(result.field_filters) == 1
@@ -72,7 +71,7 @@ class TestFieldFilters:
assert filter.value == "hobbit"
assert filter.negated is False
def test_parse_quoted_title_filter(self, parser: QueryParser):
def test_parse_quoted_title_filter(self, parser: QueryParser) -> None:
"""Test parsing quoted title field filter."""
result = parser.parse('title:"the hobbit"')
assert len(result.field_filters) == 1
@@ -80,7 +79,7 @@ class TestFieldFilters:
assert filter.field == Field.TITLE
assert filter.value == "the hobbit"
def test_parse_author_filter(self, parser: QueryParser):
def test_parse_author_filter(self, parser: QueryParser) -> None:
"""Test parsing author field filter."""
result = parser.parse("author:tolkien")
assert len(result.field_filters) == 1
@@ -88,7 +87,7 @@ class TestFieldFilters:
assert filter.field == Field.AUTHOR
assert filter.value == "tolkien"
def test_parse_negated_filter(self, parser: QueryParser):
def test_parse_negated_filter(self, parser: QueryParser) -> None:
"""Test parsing negated field filter."""
result = parser.parse("-genre:romance")
assert len(result.field_filters) == 1
@@ -97,7 +96,7 @@ class TestFieldFilters:
assert filter.value == "romance"
assert filter.negated is True
def test_parse_multiple_filters(self, parser: QueryParser):
def test_parse_multiple_filters(self, parser: QueryParser) -> None:
"""Test parsing multiple field filters."""
result = parser.parse("author:tolkien genre:fantasy")
assert len(result.field_filters) == 2
@@ -108,7 +107,7 @@ class TestFieldFilters:
genre_filter = next(f for f in result.field_filters if f.field == Field.GENRE)
assert genre_filter.value == "fantasy"
def test_parse_mixed_filters_and_text(self, parser: QueryParser):
def test_parse_mixed_filters_and_text(self, parser: QueryParser) -> None:
"""Test parsing mix of field filters and text terms."""
result = parser.parse('epic author:tolkien "middle earth"')
assert "epic" in result.text_terms
@@ -133,8 +132,11 @@ class TestComparisonOperators:
],
)
def test_parse_comparison_operators(
self, parser: QueryParser, operator_str, expected_operator
):
self,
parser: QueryParser,
operator_str: str,
expected_operator: ComparisonOperator,
) -> None:
"""Test parsing all supported comparison operators."""
query = f"rating{operator_str}4"
result = parser.parse(query)
@@ -145,7 +147,7 @@ class TestComparisonOperators:
assert filter.operator == expected_operator
assert filter.value == 4
def test_parse_date_comparison(self, parser: QueryParser):
def test_parse_date_comparison(self, parser: QueryParser) -> None:
"""Test parsing date comparison operators."""
result = parser.parse("added>=2026-03-15")
assert len(result.field_filters) == 1
@@ -154,7 +156,7 @@ class TestComparisonOperators:
assert filter.operator == ComparisonOperator.GREATER_EQUAL
assert filter.value == date(2026, 3, 15)
def test_parse_numeric_comparison(self, parser: QueryParser):
def test_parse_numeric_comparison(self, parser: QueryParser) -> None:
"""Test parsing numeric comparison operators."""
result = parser.parse("shelf>2")
assert len(result.field_filters) == 1
@@ -167,79 +169,77 @@ class TestComparisonOperators:
class TestTypeConversion:
"""Test the _convert_value method for different field types."""
def test_convert_date_field_valid(self, parser: QueryParser):
def test_convert_date_field_valid(self, parser: QueryParser) -> None:
"""Test converting valid date strings for date fields."""
result = parser._convert_value(Field.BOUGHT_DATE, "2026-03-15")
result = _convert_value(Field.BOUGHT_DATE, "2026-03-15")
assert result == date(2026, 3, 15)
result = parser._convert_value(Field.READ_DATE, "2025-12-31")
result = _convert_value(Field.READ_DATE, "2025-12-31")
assert result == date(2025, 12, 31)
result = parser._convert_value(Field.ADDED_DATE, "2024-01-01")
result = _convert_value(Field.ADDED_DATE, "2024-01-01")
assert result == date(2024, 1, 1)
def test_convert_date_field_invalid(self, parser: QueryParser):
def test_convert_date_field_invalid(self, parser: QueryParser) -> None:
"""Test converting invalid date strings falls back to string."""
result = parser._convert_value(Field.BOUGHT_DATE, "invalid-date")
result = _convert_value(Field.BOUGHT_DATE, "invalid-date")
assert result == "invalid-date"
result = parser._convert_value(
Field.READ_DATE, "2026-13-45"
) # Invalid month/day
result = _convert_value(Field.READ_DATE, "2026-13-45") # Invalid month/day
assert result == "2026-13-45"
result = parser._convert_value(Field.ADDED_DATE, "not-a-date")
result = _convert_value(Field.ADDED_DATE, "not-a-date")
assert result == "not-a-date"
def test_convert_numeric_field_integers(self, parser: QueryParser):
def test_convert_numeric_field_integers(self, parser: QueryParser) -> None:
"""Test converting integer strings for numeric fields."""
result = parser._convert_value(Field.RATING, "5")
result = _convert_value(Field.RATING, "5")
assert result == 5
assert isinstance(result, int)
result = parser._convert_value(Field.SHELF, "10")
result = _convert_value(Field.SHELF, "10")
assert result == 10
result = parser._convert_value(Field.YEAR, "2026")
result = _convert_value(Field.YEAR, "2026")
assert result == 2026
def test_convert_numeric_field_floats(self, parser: QueryParser):
def test_convert_numeric_field_floats(self, parser: QueryParser) -> None:
"""Test converting float strings for numeric fields."""
result = parser._convert_value(Field.RATING, "4.5")
assert result == 4.5
result = _convert_value(Field.RATING, "4.5")
assert result == pytest.approx(4.5)
assert isinstance(result, float)
result = parser._convert_value(Field.SHELF, "2.0")
assert result == 2.0
result = _convert_value(Field.SHELF, "2.0")
assert result == pytest.approx(2.0)
def test_convert_numeric_field_invalid(self, parser: QueryParser):
def test_convert_numeric_field_invalid(self, parser: QueryParser) -> None:
"""Test converting invalid numeric strings falls back to string."""
result = parser._convert_value(Field.RATING, "not-a-number")
result = _convert_value(Field.RATING, "not-a-number")
assert result == "not-a-number"
result = parser._convert_value(Field.SHELF, "abc")
result = _convert_value(Field.SHELF, "abc")
assert result == "abc"
result = parser._convert_value(Field.YEAR, "twenty-twenty-six")
result = _convert_value(Field.YEAR, "twenty-twenty-six")
assert result == "twenty-twenty-six"
def test_convert_string_fields(self, parser: QueryParser):
def test_convert_string_fields(self, parser: QueryParser) -> None:
"""Test converting values for string fields returns as-is."""
result = parser._convert_value(Field.TITLE, "The Hobbit")
result = _convert_value(Field.TITLE, "The Hobbit")
assert result == "The Hobbit"
result = parser._convert_value(Field.AUTHOR, "Tolkien")
result = _convert_value(Field.AUTHOR, "Tolkien")
assert result == "Tolkien"
result = parser._convert_value(Field.GENRE, "Fantasy")
result = _convert_value(Field.GENRE, "Fantasy")
assert result == "Fantasy"
# Even things that look like dates/numbers should stay as strings for string fields
result = parser._convert_value(Field.TITLE, "2026-03-15")
result = _convert_value(Field.TITLE, "2026-03-15")
assert result == "2026-03-15"
assert isinstance(result, str)
result = parser._convert_value(Field.AUTHOR, "123")
result = _convert_value(Field.AUTHOR, "123")
assert result == "123"
assert isinstance(result, str)
@@ -247,13 +247,13 @@ class TestTypeConversion:
class TestParsingEdgeCases:
"""Test edge cases and error handling in query parsing."""
def test_parse_invalid_field_name(self, parser: QueryParser):
def test_parse_invalid_field_name(self, parser: QueryParser) -> None:
"""Test parsing with invalid field names falls back to text search."""
result = parser.parse("invalid_field:value")
# Should fall back to treating the whole thing as text
assert len(result.text_terms) >= 1 or len(result.field_filters) == 0
def test_parse_mixed_quotes_and_operators(self, parser: QueryParser):
def test_parse_mixed_quotes_and_operators(self, parser: QueryParser) -> None:
"""Test parsing complex queries with quotes and operators."""
result = parser.parse('title:"The Lord" author:tolkien rating>=4')
@@ -278,35 +278,36 @@ class TestParsingEdgeCases:
assert rating_filter.value == 4
assert rating_filter.operator == ComparisonOperator.GREATER_EQUAL
def test_parse_escaped_quotes(self, parser: QueryParser):
def test_parse_escaped_quotes(self, parser: QueryParser) -> None:
"""Test parsing strings with escaped quotes."""
result = parser.parse(r'title:"She said \"hello\""')
if result.field_filters:
# If parsing succeeds, check the escaped quote handling
filter = result.field_filters[0]
assert isinstance(filter.value, str)
assert "hello" in filter.value
# If parsing fails, it should fall back gracefully
def test_parse_special_characters(self, parser: QueryParser):
def test_parse_special_characters(self, parser: QueryParser) -> None:
"""Test parsing queries with special characters."""
result = parser.parse("title:C++ author:Stroustrup")
# Should handle the + characters gracefully
assert len(result.field_filters) >= 1 or len(result.text_terms) >= 1
def test_parse_very_long_query(self, parser: QueryParser):
def test_parse_very_long_query(self, parser: QueryParser) -> None:
"""Test parsing very long query strings."""
long_value = "a" * 1000
result = parser.parse(f"title:{long_value}")
# Should handle long strings without crashing
assert isinstance(result, SearchQuery)
def test_parse_unicode_characters(self, parser: QueryParser):
def test_parse_unicode_characters(self, parser: QueryParser) -> None:
"""Test parsing queries with unicode characters."""
result = parser.parse("title:Café author:José")
# Should handle unicode gracefully
assert isinstance(result, SearchQuery)
def test_fallback_behavior_on_parse_error(self, parser: QueryParser):
def test_fallback_behavior_on_parse_error(self, parser: QueryParser) -> None:
"""Test that invalid syntax falls back to text search."""
# Construct a query that should cause parse errors
invalid_queries = [
@@ -327,7 +328,7 @@ class TestParsingEdgeCases:
class TestComplexQueries:
"""Test parsing of complex, real-world query examples."""
def test_parse_realistic_book_search(self, parser: QueryParser):
def test_parse_realistic_book_search(self, parser: QueryParser) -> None:
"""Test parsing realistic book search queries."""
result = parser.parse(
'author:tolkien genre:fantasy -genre:romance rating>=4 "middle earth"'
@@ -363,7 +364,7 @@ class TestComplexQueries:
assert romance_filter.value == "romance"
assert romance_filter.negated is True
def test_parse_location_and_date_filters(self, parser: QueryParser):
def test_parse_location_and_date_filters(self, parser: QueryParser) -> None:
"""Test parsing location and date-based queries."""
result = parser.parse("place:home bookshelf:fantasy shelf>=2 added>=2026-01-01")
@@ -372,21 +373,24 @@ class TestComplexQueries:
place_filter = next(
(f for f in result.field_filters if f.field == Field.PLACE), None
)
assert place_filter is not None
assert place_filter.value == "home"
shelf_filter = next(
(f for f in result.field_filters if f.field == Field.SHELF), None
)
assert shelf_filter is not None
assert shelf_filter.value == 2
assert shelf_filter.operator == ComparisonOperator.GREATER_EQUAL
added_filter = next(
(f for f in result.field_filters if f.field == Field.ADDED_DATE), None
)
assert added_filter is not None
assert added_filter.value == date(2026, 1, 1)
assert added_filter.operator == ComparisonOperator.GREATER_EQUAL
def test_parse_mixed_types_comprehensive(self, parser: QueryParser):
def test_parse_mixed_types_comprehensive(self, parser: QueryParser) -> None:
"""Test parsing query with all major field types."""
query = 'title:"Complex Book" author:Author year=2020 rating>=4 bought<=2025-12-31 -genre:boring epic adventure'
result = parser.parse(query)