|
8 | 8 | import pytest
|
9 | 9 |
|
10 | 10 | from libtmux._internal.query_list import (
|
| 11 | + LOOKUP_NAME_MAP, |
11 | 12 | MultipleObjectsReturned,
|
12 | 13 | ObjectDoesNotExist,
|
13 | 14 | PKRequiredException,
|
14 | 15 | QueryList,
|
15 | 16 | keygetter,
|
16 | 17 | lookup_contains,
|
| 18 | + lookup_endswith, |
17 | 19 | lookup_exact,
|
18 | 20 | lookup_icontains,
|
| 21 | + lookup_iendswith, |
19 | 22 | lookup_iexact,
|
20 | 23 | lookup_in,
|
21 | 24 | lookup_iregex,
|
| 25 | + lookup_istartswith, |
22 | 26 | lookup_nin,
|
23 | 27 | lookup_regex,
|
| 28 | + lookup_startswith, |
24 | 29 | parse_lookup,
|
25 | 30 | )
|
26 | 31 |
|
@@ -619,3 +624,197 @@ def test_filter_error_handling() -> None:
|
619 | 624 | empty_args: dict[str, t.Any] = {"": "test"}
|
620 | 625 | result = ql.filter(**empty_args)
|
621 | 626 | assert len(result) == 0
|
| 627 | + |
| 628 | + |
| 629 | +def test_lookup_startswith_endswith_functions() -> None: |
| 630 | + """Test startswith and endswith lookup functions with various types.""" |
| 631 | + # Test lookup_startswith |
| 632 | + assert lookup_startswith("test123", "test") # Basic match |
| 633 | + assert not lookup_startswith("test123", "123") # No match at start |
| 634 | + assert not lookup_startswith(["test"], "test") # Invalid type for data |
| 635 | + assert not lookup_startswith("test", ["test"]) # Invalid type for rhs |
| 636 | + assert not lookup_startswith("test", 123) # type: ignore # Invalid type for rhs |
| 637 | + |
| 638 | + # Test lookup_istartswith |
| 639 | + assert lookup_istartswith("TEST123", "test") # Case-insensitive match |
| 640 | + assert lookup_istartswith("test123", "TEST") # Case-insensitive match reverse |
| 641 | + assert not lookup_istartswith("test123", "123") # No match at start |
| 642 | + assert not lookup_istartswith(["test"], "test") # Invalid type for data |
| 643 | + assert not lookup_istartswith("test", ["test"]) # Invalid type for rhs |
| 644 | + assert not lookup_istartswith("test", 123) # type: ignore # Invalid type for rhs |
| 645 | + |
| 646 | + # Test lookup_endswith |
| 647 | + assert lookup_endswith("test123", "123") # Basic match |
| 648 | + assert not lookup_endswith("test123", "test") # No match at end |
| 649 | + assert not lookup_endswith(["test"], "test") # Invalid type for data |
| 650 | + assert not lookup_endswith("test", ["test"]) # Invalid type for rhs |
| 651 | + assert not lookup_endswith("test", 123) # type: ignore # Invalid type for rhs |
| 652 | + |
| 653 | + # Test lookup_iendswith |
| 654 | + assert lookup_iendswith("test123", "123") # Basic match |
| 655 | + assert lookup_iendswith("test123", "123") # Case-insensitive match |
| 656 | + assert lookup_iendswith("test123", "123") # Case-insensitive match reverse |
| 657 | + assert not lookup_iendswith("test123", "test") # No match at end |
| 658 | + assert not lookup_iendswith(["test"], "test") # Invalid type for data |
| 659 | + assert not lookup_iendswith("test", ["test"]) # Invalid type for rhs |
| 660 | + assert not lookup_iendswith("test", 123) # type: ignore # Invalid type for rhs |
| 661 | + |
| 662 | + |
| 663 | +def test_query_list_eq_numeric_comparison() -> None: |
| 664 | + """Test QueryList __eq__ method with numeric comparisons.""" |
| 665 | + # Test exact numeric matches |
| 666 | + ql1 = QueryList([{"a": 1, "b": 2.0}]) |
| 667 | + ql2 = QueryList([{"a": 1, "b": 2.0}]) |
| 668 | + assert ql1 == ql2 |
| 669 | + |
| 670 | + # Test numeric comparison within tolerance (difference < 1) |
| 671 | + ql3 = QueryList([{"a": 1.1, "b": 2.1}]) |
| 672 | + assert ql1 == ql3 # Should be equal since difference is less than 1 |
| 673 | + |
| 674 | + # Test numeric comparison outside tolerance (difference > 1) |
| 675 | + ql4 = QueryList([{"a": 2.5, "b": 3.5}]) |
| 676 | + assert ql1 != ql4 # Should not be equal since difference is more than 1 |
| 677 | + |
| 678 | + # Test mixed numeric types |
| 679 | + ql5 = QueryList([{"a": 1, "b": 2}]) # int instead of float |
| 680 | + assert ql1 == ql5 # Should be equal since values are equivalent |
| 681 | + |
| 682 | + # Test with nested numeric values |
| 683 | + ql6 = QueryList([{"a": {"x": 1.0, "y": 2.0}}]) |
| 684 | + ql7 = QueryList([{"a": {"x": 1.1, "y": 2.1}}]) |
| 685 | + assert ql6 == ql7 # Should be equal since differences are less than 1 |
| 686 | + |
| 687 | + # Test with mixed content |
| 688 | + ql10 = QueryList([{"a": 1, "b": "test"}]) |
| 689 | + ql11 = QueryList([{"a": 1.1, "b": "test"}]) |
| 690 | + assert ql10 == ql11 # Should be equal since numeric difference is less than 1 |
| 691 | + |
| 692 | + # Test with non-dict content (exact equality required) |
| 693 | + ql8 = QueryList([1, 2, 3]) |
| 694 | + ql9 = QueryList([1, 2, 3]) |
| 695 | + assert ql8 == ql9 # Should be equal since values are exactly the same |
| 696 | + assert ql8 != QueryList( |
| 697 | + [1.1, 2.1, 3.1] |
| 698 | + ) # Should not be equal since values are different |
| 699 | + |
| 700 | + |
| 701 | +def test_keygetter_nested_objects() -> None: |
| 702 | + """Test keygetter function with nested objects.""" |
| 703 | + |
| 704 | + @dataclasses.dataclass |
| 705 | + class Food: |
| 706 | + fruit: list[str] = dataclasses.field(default_factory=list) |
| 707 | + breakfast: str | None = None |
| 708 | + |
| 709 | + @dataclasses.dataclass |
| 710 | + class Restaurant: |
| 711 | + place: str |
| 712 | + city: str |
| 713 | + state: str |
| 714 | + food: Food = dataclasses.field(default_factory=Food) |
| 715 | + |
| 716 | + # Test with nested dataclass |
| 717 | + restaurant = Restaurant( |
| 718 | + place="Largo", |
| 719 | + city="Tampa", |
| 720 | + state="Florida", |
| 721 | + food=Food(fruit=["banana", "orange"], breakfast="cereal"), |
| 722 | + ) |
| 723 | + assert keygetter(restaurant, "food") == Food( |
| 724 | + fruit=["banana", "orange"], breakfast="cereal" |
| 725 | + ) |
| 726 | + assert keygetter(restaurant, "food__breakfast") == "cereal" |
| 727 | + assert keygetter(restaurant, "food__fruit") == ["banana", "orange"] |
| 728 | + |
| 729 | + # Test with non-existent attribute (returns None due to exception handling) |
| 730 | + with suppress(Exception): |
| 731 | + assert keygetter(restaurant, "nonexistent") is None |
| 732 | + |
| 733 | + # Test with invalid path format (returns the object itself) |
| 734 | + assert keygetter(restaurant, "") == restaurant |
| 735 | + assert keygetter(restaurant, "__") == restaurant |
| 736 | + |
| 737 | + # Test with non-mapping object (returns the object itself) |
| 738 | + non_mapping = "not a mapping" |
| 739 | + assert keygetter(non_mapping, "any_key") == non_mapping # type: ignore |
| 740 | + |
| 741 | + |
| 742 | +def test_query_list_slicing() -> None: |
| 743 | + """Test QueryList slicing operations.""" |
| 744 | + ql = QueryList([1, 2, 3, 4, 5]) |
| 745 | + |
| 746 | + # Test positive indices |
| 747 | + assert ql[1:3] == QueryList([2, 3]) |
| 748 | + assert ql[0:5:2] == QueryList([1, 3, 5]) |
| 749 | + |
| 750 | + # Test negative indices |
| 751 | + assert ql[-3:] == QueryList([3, 4, 5]) |
| 752 | + assert ql[:-2] == QueryList([1, 2, 3]) |
| 753 | + assert ql[-4:-2] == QueryList([2, 3]) |
| 754 | + |
| 755 | + # Test steps |
| 756 | + assert ql[::2] == QueryList([1, 3, 5]) |
| 757 | + assert ql[::-1] == QueryList([5, 4, 3, 2, 1]) |
| 758 | + assert ql[4:0:-2] == QueryList([5, 3]) |
| 759 | + |
| 760 | + # Test empty slices |
| 761 | + assert ql[5:] == QueryList([]) |
| 762 | + assert ql[-1:-5] == QueryList([]) |
| 763 | + |
| 764 | + |
| 765 | +def test_query_list_attributes() -> None: |
| 766 | + """Test QueryList list behavior and pk_key attribute.""" |
| 767 | + # Test list behavior |
| 768 | + ql = QueryList([1, 2, 3]) |
| 769 | + assert list(ql) == [1, 2, 3] |
| 770 | + assert len(ql) == 3 |
| 771 | + assert ql[0] == 1 |
| 772 | + assert ql[-1] == 3 |
| 773 | + |
| 774 | + # Test pk_key attribute with objects |
| 775 | + @dataclasses.dataclass |
| 776 | + class Item: |
| 777 | + id: str |
| 778 | + value: int |
| 779 | + |
| 780 | + items = [Item("1", 1), Item("2", 2)] |
| 781 | + ql = QueryList(items) |
| 782 | + ql.pk_key = "id" |
| 783 | + assert ql.items() == [("1", items[0]), ("2", items[1])] |
| 784 | + |
| 785 | + # Test pk_key with non-existent attribute |
| 786 | + ql.pk_key = "nonexistent" |
| 787 | + with pytest.raises(AttributeError): |
| 788 | + ql.items() |
| 789 | + |
| 790 | + # Test pk_key with None |
| 791 | + ql.pk_key = None |
| 792 | + with pytest.raises(PKRequiredException): |
| 793 | + ql.items() |
| 794 | + |
| 795 | + |
| 796 | +def test_lookup_name_map() -> None: |
| 797 | + """Test LOOKUP_NAME_MAP contains all lookup functions.""" |
| 798 | + # Test all lookup functions are in the map |
| 799 | + assert LOOKUP_NAME_MAP["eq"] == lookup_exact |
| 800 | + assert LOOKUP_NAME_MAP["exact"] == lookup_exact |
| 801 | + assert LOOKUP_NAME_MAP["iexact"] == lookup_iexact |
| 802 | + assert LOOKUP_NAME_MAP["contains"] == lookup_contains |
| 803 | + assert LOOKUP_NAME_MAP["icontains"] == lookup_icontains |
| 804 | + assert LOOKUP_NAME_MAP["startswith"] == lookup_startswith |
| 805 | + assert LOOKUP_NAME_MAP["istartswith"] == lookup_istartswith |
| 806 | + assert LOOKUP_NAME_MAP["endswith"] == lookup_endswith |
| 807 | + assert LOOKUP_NAME_MAP["iendswith"] == lookup_iendswith |
| 808 | + assert LOOKUP_NAME_MAP["in"] == lookup_in |
| 809 | + assert LOOKUP_NAME_MAP["nin"] == lookup_nin |
| 810 | + assert LOOKUP_NAME_MAP["regex"] == lookup_regex |
| 811 | + assert LOOKUP_NAME_MAP["iregex"] == lookup_iregex |
| 812 | + |
| 813 | + # Test lookup functions behavior through the map |
| 814 | + data = "test123" |
| 815 | + assert LOOKUP_NAME_MAP["contains"](data, "test") |
| 816 | + assert LOOKUP_NAME_MAP["icontains"](data, "TEST") |
| 817 | + assert LOOKUP_NAME_MAP["startswith"](data, "test") |
| 818 | + assert LOOKUP_NAME_MAP["endswith"](data, "123") |
| 819 | + assert not LOOKUP_NAME_MAP["in"](data, ["other", "values"]) |
| 820 | + assert LOOKUP_NAME_MAP["regex"](data, r"\d+") |
0 commit comments