Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!(QueryList): Generic fixes #515

Merged
merged 8 commits into from Feb 7, 2024
Merged

fix!(QueryList): Generic fixes #515

merged 8 commits into from Feb 7, 2024

Conversation

tony
Copy link
Member

@tony tony commented Feb 7, 2024

ChatGPT helped!

Part 1

Prompt

Assume this:

"""Utilities for filtering or searching :class:`list` of objects / list data.

Note
----
This is an internal API not covered by versioning policy.
"""
import re
import traceback
import typing as t
from collections.abc import Mapping, Sequence

if t.TYPE_CHECKING:

    class LookupProtocol(t.Protocol):
        """Protocol for :class:`QueryList` filtering operators."""

        def __call__(
            self,
            data: t.Union[str, t.List[str], "Mapping[str, str]"],
            rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
        ) -> bool:
            """Return callback for :class:`QueryList` filtering operators."""
            ...


T = t.TypeVar("T", t.Any, t.Any)

no_arg = object()


class MultipleObjectsReturned(Exception):
    """The requested object does not exist."""


class ObjectDoesNotExist(Exception):
    """The query returned multiple objects when only one was expected."""


def keygetter(
    obj: "Mapping[str, t.Any]",
    path: str,
) -> t.Union[None, t.Any, str, t.List[str], "Mapping[str, str]"]:
    """obj, "foods__breakfast", obj['foods']['breakfast'].

    >>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods__breakfast")
    'cereal'
    >>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods")
    {'breakfast': 'cereal'}

    """
    try:
        sub_fields = path.split("__")
        dct = obj
        for sub_field in sub_fields:
            if isinstance(dct, dict):
                dct = dct[sub_field]
            elif hasattr(dct, sub_field):
                dct = getattr(dct, sub_field)

    except Exception as e:
        traceback.print_stack()
        print(f"Above error was {e}")
        return None

    return dct


def parse_lookup(
    obj: "Mapping[str, t.Any]",
    path: str,
    lookup: str,
) -> t.Optional[t.Any]:
    """Check if field lookup key, e.g. "my__path__contains" has comparator, return val.

    If comparator not used or value not found, return None.

    mykey__endswith("mykey") -> "mykey" else None

    >>> parse_lookup({ "food": "red apple" }, "food__istartswith", "__istartswith")
    'red apple'
    """
    try:
        if isinstance(path, str) and isinstance(lookup, str) and path.endswith(lookup):
            field_name = path.rsplit(lookup)[0]
            if field_name is not None:
                return keygetter(obj, field_name)
    except Exception:
        traceback.print_stack()
    return None


def lookup_exact(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    return rhs == data


def lookup_iexact(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False

    return rhs.lower() == data.lower()


def lookup_contains(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, (str, Mapping, list)):
        return False

    return rhs in data


def lookup_icontains(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, (str, Mapping, list)):
        return False

    if isinstance(data, str):
        return rhs.lower() in data.lower()
    if isinstance(data, Mapping):
        return rhs.lower() in [k.lower() for k in data]

    return False


def lookup_startswith(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False

    return data.startswith(rhs)


def lookup_istartswith(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False

    return data.lower().startswith(rhs.lower())


def lookup_endswith(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False

    return data.endswith(rhs)


def lookup_iendswith(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False
    return data.lower().endswith(rhs.lower())


def lookup_in(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if isinstance(rhs, list):
        return data in rhs

    try:
        if isinstance(rhs, str) and isinstance(data, Mapping):
            return rhs in data
        if isinstance(rhs, str) and isinstance(data, (str, list)):
            return rhs in data
        if isinstance(rhs, str) and isinstance(data, Mapping):
            return rhs in data
        # TODO: Add a deep Mappingionary matcher
        # if isinstance(rhs, Mapping) and isinstance(data, Mapping):
        #     return rhs.items() not in data.items()
    except Exception:
        return False
    return False


def lookup_nin(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if isinstance(rhs, list):
        return data not in rhs

    try:
        if isinstance(rhs, str) and isinstance(data, Mapping):
            return rhs not in data
        if isinstance(rhs, str) and isinstance(data, (str, list)):
            return rhs not in data
        if isinstance(rhs, str) and isinstance(data, Mapping):
            return rhs not in data
        # TODO: Add a deep Mappingionary matcher
        # if isinstance(rhs, Mapping) and isinstance(data, Mapping):
        #     return rhs.items() not in data.items()
    except Exception:
        return False
    return False


def lookup_regex(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if isinstance(data, (str, bytes, re.Pattern)) and isinstance(rhs, (str, bytes)):
        return bool(re.search(rhs, data))
    return False


def lookup_iregex(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if isinstance(data, (str, bytes, re.Pattern)) and isinstance(rhs, (str, bytes)):
        return bool(re.search(rhs, data, re.IGNORECASE))
    return False


LOOKUP_NAME_MAP: 'Mapping[str, "LookupProtocol"]' = {
    "eq": lookup_exact,
    "exact": lookup_exact,
    "iexact": lookup_iexact,
    "contains": lookup_contains,
    "icontains": lookup_icontains,
    "startswith": lookup_startswith,
    "istartswith": lookup_istartswith,
    "endswith": lookup_endswith,
    "iendswith": lookup_iendswith,
    "in": lookup_in,
    "nin": lookup_nin,
    "regex": lookup_regex,
    "iregex": lookup_iregex,
}


class PKRequiredException(Exception):
    def __init__(self, *args: object):
        return super().__init__("items() require a pk_key exists")


class OpNotFound(ValueError):
    def __init__(self, op: str, *args: object):
        return super().__init__(f"{op} not in LOOKUP_NAME_MAP")


class QueryList(t.List[T]):
    """Filter list of object/dictionaries. For small, local datasets.

    *Experimental, unstable*.

    >>> query = QueryList(
    ...     [
    ...         {
    ...             "place": "Largo",
    ...             "city": "Tampa",
    ...             "state": "Florida",
    ...             "foods": {"fruit": ["banana", "orange"], "breakfast": "cereal"},
    ...         },
    ...         {
    ...             "place": "Chicago suburbs",
    ...             "city": "Elmhurst",
    ...             "state": "Illinois",
    ...             "foods": {"fruit": ["apple", "cantelope"], "breakfast": "waffles"},
    ...         },
    ...     ]
    ... )
    >>> query.filter(place="Chicago suburbs")[0]['city']
    'Elmhurst'
    >>> query.filter(place__icontains="chicago")[0]['city']
    'Elmhurst'
    >>> query.filter(foods__breakfast="waffles")[0]['city']
    'Elmhurst'
    >>> query.filter(foods__fruit__in="cantelope")[0]['city']
    'Elmhurst'
    >>> query.filter(foods__fruit__in="orange")[0]['city']
    'Tampa'
    >>> query.get(foods__fruit__in="orange")['city']
    'Tampa'
    """

    data: "Sequence[T]"
    pk_key: t.Optional[str]

    def items(self) -> t.List[T]:
        if self.pk_key is None:
            raise PKRequiredException()
        return [(getattr(item, self.pk_key), item) for item in self]

    def __eq__(
        self,
        other: object,
        # other: t.Union[
        #     "QueryList[T]",
        #     t.List[Mapping[str, str]],
        #     t.List[Mapping[str, int]],
        #     t.List[Mapping[str, t.Union[str, Mapping[str, t.Union[List[str], str]]]]],
        # ],
    ) -> bool:
        data = other

        if not isinstance(self, list) or not isinstance(data, list):
            return False

        if len(self) == len(data):
            for a, b in zip(self, data):
                if isinstance(a, Mapping):
                    a_keys = a.keys()
                    if a.keys == b.keys():
                        for key in a_keys:
                            if abs(a[key] - b[key]) > 1:
                                return False
                else:
                    if a != b:
                        return False

            return True
        return False

    def filter(
        self,
        matcher: t.Optional[t.Union[t.Callable[[T], bool], T]] = None,
        **kwargs: t.Any,
    ) -> "QueryList[T]":
        """Filter list of objects."""

        def filter_lookup(obj: t.Any) -> bool:
            for path, v in kwargs.items():
                try:
                    lhs, op = path.rsplit("__", 1)

                    if op not in LOOKUP_NAME_MAP:
                        raise OpNotFound(op=op)
                except ValueError:
                    lhs = path
                    op = "exact"

                assert op in LOOKUP_NAME_MAP
                path = lhs
                data = keygetter(obj, path)

                if data is None or not LOOKUP_NAME_MAP[op](data, v):
                    return False

            return True

        if callable(matcher):
            _filter = matcher
        elif matcher is not None:

            def val_match(obj: t.Union[str, t.List[t.Any]]) -> bool:
                if isinstance(matcher, list):
                    return obj in matcher
                else:
                    return bool(obj == matcher)

            _filter = val_match
        else:
            _filter = filter_lookup

        return self.__class__(k for k in self if _filter(k))

    def get(
        self,
        matcher: t.Optional[t.Union[t.Callable[[T], bool], T]] = None,
        default: t.Optional[t.Any] = no_arg,
        **kwargs: t.Any,
    ) -> t.Optional[T]:
        """Retrieve one object.

        Raises :exc:`MultipleObjectsReturned` if multiple objects found.

        Raises :exc:`ObjectDoesNotExist` if no object found, unless ``default`` stated.
        """
        objs = self.filter(matcher=matcher, **kwargs)
        if len(objs) > 1:
            raise MultipleObjectsReturned()
        elif len(objs) == 0:
            if default == no_arg:
                raise ObjectDoesNotExist()
            return default
        return objs[0]

What would it take to make QueryList behave as a generic list type? Right now (as-is) QueryList[CustomObjectType] is not a valid annotation according to mypy.

Response:

Response

To make QueryList behave as a generic list type and ensure it's recognized as such by static type checkers like mypy, you need to ensure that it correctly inherits from a generic type and uses type annotations properly throughout its definition. The modifications needed involve leveraging the typing system to annotate QueryList as a generic collection properly.

Here are the steps to make QueryList a properly typed, generic list in Python:

  1. Ensure Proper Inheritance: The QueryList should inherit from typing.Generic and collections.abc.Sequence (or another appropriate base class) to get list-like behavior while also being generic. You've already imported Generic and are using TypeVar, which is great. However, your QueryList definition should explicitly indicate it's generic over some type T.

  2. Use the TypeVar Correctly: You declared a TypeVar named T but didn't use it effectively within QueryList to ensure it's recognized as a generic class by type checkers. You should use T as a placeholder for the type of items contained in the QueryList.

  3. Annotate data Attribute: The data attribute in QueryList should be annotated to indicate it holds a sequence of type T elements. However, this attribute seems redundant if QueryList itself behaves like a list; you might be intending to use self to store the data directly.

  4. Correct Method Annotations: Ensure all methods that operate on elements of the list use the T type variable appropriately. This includes methods that add, remove, retrieve, or operate on elements in any way, ensuring they are compatible with the generic type T.

  5. Update __init__ Method: Your QueryList doesn't explicitly define an __init__ method to initialize the list. It's important to properly initialize the internal list structure that will hold the data.

Below is an updated example that incorporates these suggestions:

from typing import TypeVar, Generic, List, Optional, Callable, Any, Union, Sequence
from collections.abc import Iterable
import re

T = TypeVar('T')

class QueryList(Generic[T], List[T]):
    def __init__(self, items: Optional[Iterable[T]] = None) -> None:
        super().__init__(items if items is not None else [])

    # Example method updated to use type variable T
    def filter(self, matcher: Optional[Union[Callable[[T], bool], T]] = None, **kwargs: Any) -> 'QueryList[T]':
        filtered_list = [item for item in self if self._match(item, matcher, **kwargs)]
        return QueryList(filtered_list)

    def _match(self, item: T, matcher: Optional[Union[Callable[[T], bool], T]], **kwargs: Any) -> bool:
        # Implement matching logic here
        return True

    # Ensure other methods also use the T type variable where appropriate

This example assumes QueryList directly inherits list-like behavior from List[T] and adds typing information to make it a generic class capable of holding items of any specified type. It demonstrates how to initialize it with items of type T and includes a placeholder for a filter method that should be implemented according to your specific needs.

Remember, when you're using QueryList[CustomObjectType], the CustomObjectType needs to be a class or type alias that mypy can recognize. Also, make sure your runtime logic correctly handles the types you expect QueryList to contain, as Python's type annotations are not enforced at runtime.

Part 2

Prompt

Consider the following:

"""Utilities for filtering or searching :class:`list` of objects / list data.

Note
----
This is an internal API not covered by versioning policy.
"""
import re
import traceback
import typing as t
from collections.abc import Iterable, Mapping, Sequence

if t.TYPE_CHECKING:

    class LookupProtocol(t.Protocol):
        """Protocol for :class:`QueryList` filtering operators."""

        def __call__(
            self,
            data: t.Union[str, t.List[str], "Mapping[str, str]"],
            rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
        ) -> bool:
            """Return callback for :class:`QueryList` filtering operators."""
            ...


T = t.TypeVar("T")
# T = t.TypeVar("T", t.Any, t.Any)

no_arg = object()


class MultipleObjectsReturned(Exception):
    """The requested object does not exist."""


class ObjectDoesNotExist(Exception):
    """The query returned multiple objects when only one was expected."""


def keygetter(
    obj: "Mapping[str, t.Any]",
    path: str,
) -> t.Union[None, t.Any, str, t.List[str], "Mapping[str, str]"]:
    """obj, "foods__breakfast", obj['foods']['breakfast'].

    >>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods__breakfast")
    'cereal'
    >>> keygetter({ "foods": { "breakfast": "cereal" } }, "foods")
    {'breakfast': 'cereal'}

    """
    try:
        sub_fields = path.split("__")
        dct = obj
        for sub_field in sub_fields:
            if isinstance(dct, dict):
                dct = dct[sub_field]
            elif hasattr(dct, sub_field):
                dct = getattr(dct, sub_field)

    except Exception as e:
        traceback.print_stack()
        print(f"Above error was {e}")
        return None

    return dct


def parse_lookup(
    obj: "Mapping[str, t.Any]",
    path: str,
    lookup: str,
) -> t.Optional[t.Any]:
    """Check if field lookup key, e.g. "my__path__contains" has comparator, return val.

    If comparator not used or value not found, return None.

    mykey__endswith("mykey") -> "mykey" else None

    >>> parse_lookup({ "food": "red apple" }, "food__istartswith", "__istartswith")
    'red apple'
    """
    try:
        if isinstance(path, str) and isinstance(lookup, str) and path.endswith(lookup):
            field_name = path.rsplit(lookup)[0]
            if field_name is not None:
                return keygetter(obj, field_name)
    except Exception:
        traceback.print_stack()
    return None


def lookup_exact(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    return rhs == data


def lookup_iexact(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False

    return rhs.lower() == data.lower()


def lookup_contains(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, (str, Mapping, list)):
        return False

    return rhs in data


def lookup_icontains(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, (str, Mapping, list)):
        return False

    if isinstance(data, str):
        return rhs.lower() in data.lower()
    if isinstance(data, Mapping):
        return rhs.lower() in [k.lower() for k in data]

    return False


def lookup_startswith(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False

    return data.startswith(rhs)


def lookup_istartswith(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False

    return data.lower().startswith(rhs.lower())


def lookup_endswith(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False

    return data.endswith(rhs)


def lookup_iendswith(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if not isinstance(rhs, str) or not isinstance(data, str):
        return False
    return data.lower().endswith(rhs.lower())


def lookup_in(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if isinstance(rhs, list):
        return data in rhs

    try:
        if isinstance(rhs, str) and isinstance(data, Mapping):
            return rhs in data
        if isinstance(rhs, str) and isinstance(data, (str, list)):
            return rhs in data
        if isinstance(rhs, str) and isinstance(data, Mapping):
            return rhs in data
        # TODO: Add a deep Mappingionary matcher
        # if isinstance(rhs, Mapping) and isinstance(data, Mapping):
        #     return rhs.items() not in data.items()
    except Exception:
        return False
    return False


def lookup_nin(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if isinstance(rhs, list):
        return data not in rhs

    try:
        if isinstance(rhs, str) and isinstance(data, Mapping):
            return rhs not in data
        if isinstance(rhs, str) and isinstance(data, (str, list)):
            return rhs not in data
        if isinstance(rhs, str) and isinstance(data, Mapping):
            return rhs not in data
        # TODO: Add a deep Mappingionary matcher
        # if isinstance(rhs, Mapping) and isinstance(data, Mapping):
        #     return rhs.items() not in data.items()
    except Exception:
        return False
    return False


def lookup_regex(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if isinstance(data, (str, bytes, re.Pattern)) and isinstance(rhs, (str, bytes)):
        return bool(re.search(rhs, data))
    return False


def lookup_iregex(
    data: t.Union[str, t.List[str], "Mapping[str, str]"],
    rhs: t.Union[str, t.List[str], "Mapping[str, str]", "re.Pattern[str]"],
) -> bool:
    if isinstance(data, (str, bytes, re.Pattern)) and isinstance(rhs, (str, bytes)):
        return bool(re.search(rhs, data, re.IGNORECASE))
    return False


LOOKUP_NAME_MAP: 'Mapping[str, "LookupProtocol"]' = {
    "eq": lookup_exact,
    "exact": lookup_exact,
    "iexact": lookup_iexact,
    "contains": lookup_contains,
    "icontains": lookup_icontains,
    "startswith": lookup_startswith,
    "istartswith": lookup_istartswith,
    "endswith": lookup_endswith,
    "iendswith": lookup_iendswith,
    "in": lookup_in,
    "nin": lookup_nin,
    "regex": lookup_regex,
    "iregex": lookup_iregex,
}


class PKRequiredException(Exception):
    def __init__(self, *args: object):
        return super().__init__("items() require a pk_key exists")


class OpNotFound(ValueError):
    def __init__(self, op: str, *args: object):
        return super().__init__(f"{op} not in LOOKUP_NAME_MAP")


class QueryList(t.Generic[T], t.List[T]):
    """Filter list of object/dictionaries. For small, local datasets.

    *Experimental, unstable*.

    >>> query = QueryList(
    ...     [
    ...         {
    ...             "place": "Largo",
    ...             "city": "Tampa",
    ...             "state": "Florida",
    ...             "foods": {"fruit": ["banana", "orange"], "breakfast": "cereal"},
    ...         },
    ...         {
    ...             "place": "Chicago suburbs",
    ...             "city": "Elmhurst",
    ...             "state": "Illinois",
    ...             "foods": {"fruit": ["apple", "cantelope"], "breakfast": "waffles"},
    ...         },
    ...     ]
    ... )
    >>> query.filter(place="Chicago suburbs")[0]['city']
    'Elmhurst'
    >>> query.filter(place__icontains="chicago")[0]['city']
    'Elmhurst'
    >>> query.filter(foods__breakfast="waffles")[0]['city']
    'Elmhurst'
    >>> query.filter(foods__fruit__in="cantelope")[0]['city']
    'Elmhurst'
    >>> query.filter(foods__fruit__in="orange")[0]['city']
    'Tampa'
    >>> query.get(foods__fruit__in="orange")['city']
    'Tampa'
    """

    data: "Sequence[T]"
    pk_key: t.Optional[str]

    def __init__(self, items: t.Optional[Iterable[T]] = None) -> None:
        super().__init__(items if items is not None else [])

    def items(self) -> t.List[T]:
        if self.pk_key is None:
            raise PKRequiredException()
        return [(getattr(item, self.pk_key), item) for item in self]

    def __eq__(
        self,
        other: object,
        # other: t.Union[
        #     "QueryList[T]",
        #     t.List[Mapping[str, str]],
        #     t.List[Mapping[str, int]],
        #     t.List[Mapping[str, t.Union[str, Mapping[str, t.Union[List[str], str]]]]],
        # ],
    ) -> bool:
        data = other

        if not isinstance(self, list) or not isinstance(data, list):
            return False

        if len(self) == len(data):
            for a, b in zip(self, data):
                if isinstance(a, Mapping):
                    a_keys = a.keys()
                    if a.keys == b.keys():
                        for key in a_keys:
                            if abs(a[key] - b[key]) > 1:
                                return False
                else:
                    if a != b:
                        return False

            return True
        return False

    def filter(
        self,
        matcher: t.Optional[t.Union[t.Callable[[T], bool], T]] = None,
        **kwargs: t.Any,
    ) -> "QueryList[T]":
        """Filter list of objects."""

        def filter_lookup(obj: t.Any) -> bool:
            for path, v in kwargs.items():
                try:
                    lhs, op = path.rsplit("__", 1)

                    if op not in LOOKUP_NAME_MAP:
                        raise OpNotFound(op=op)
                except ValueError:
                    lhs = path
                    op = "exact"

                assert op in LOOKUP_NAME_MAP
                path = lhs
                data = keygetter(obj, path)

                if data is None or not LOOKUP_NAME_MAP[op](data, v):
                    return False

            return True

        if callable(matcher):
            _filter = matcher
        elif matcher is not None:

            def val_match(obj: t.Union[str, t.List[t.Any]]) -> bool:
                if isinstance(matcher, list):
                    return obj in matcher
                else:
                    return bool(obj == matcher)

            _filter = val_match
        else:
            _filter = filter_lookup

        return self.__class__(k for k in self if _filter(k))

    def get(
        self,
        matcher: t.Optional[t.Union[t.Callable[[T], bool], T]] = None,
        default: t.Optional[t.Any] = no_arg,
        **kwargs: t.Any,
    ) -> t.Optional[T]:
        """Retrieve one object.

        Raises :exc:`MultipleObjectsReturned` if multiple objects found.

        Raises :exc:`ObjectDoesNotExist` if no object found, unless ``default`` stated.
        """
        objs = self.filter(matcher=matcher, **kwargs)
        if len(objs) > 1:
            raise MultipleObjectsReturned()
        elif len(objs) == 0:
            if default == no_arg:
                raise ObjectDoesNotExist()
            return default
        return objs[0]

What do I need to do to solve these mypy warnings:

src/libtmux/_internal/query_list.py:307: error: List comprehension has incompatible type List[tuple[Any, T]]; expected List[T]  [misc]
src/libtmux/_internal/query_list.py:376: error: Incompatible types in assignment (expression has type "Callable[[str | list[Any]], bool]", variable has type "Callable[[T], bool]")  [assignment]

Response:

Response

To resolve the mypy warnings you're encountering, you need to address type compatibility and the correct use of generics in your QueryList class. Let's go through each error and discuss the solutions:

Error 1: List comprehension has incompatible type List[tuple[Any, T]]; expected List[T]

This error occurs in the items method. The method signature implies it returns a list of type T, but the actual implementation returns a list of tuples. To fix this, you should adjust either the method's return type to accurately reflect the returned data or change the method's functionality to match the expected return type. Since the method's purpose seems to be returning items along with their primary keys, adjusting the return type annotation is the appropriate solution.

Solution:

def items(self) -> t.List[t.Tuple[Any, T]]:
    if self.pk_key is None:
        raise PKRequiredException()
    return [(getattr(item, self.pk_key), item) for item in self]

Error 2: Incompatible types in assignment

This error indicates a mismatch in the expected callable type in your filter method. The _filter variable is expected to be a callable that takes an item of type T and returns a boolean. However, the assignment to val_match doesn't match this signature because val_match is defined to take either a string or a list of any type. This discrepancy arises due to the flexible nature of matcher, which can be a list, affecting the expected type of objects passed to _filter.

Solution:
To resolve this, you should ensure that the lambda or function assigned to _filter always matches the expected signature, (T) -> bool. If matcher can be of different types, handle the type checking inside the callable, ensuring it always accepts an argument of type T and returns a boolean.

if callable(matcher):
    _filter = matcher
elif matcher is not None:
    def val_match(obj: T) -> bool:
        if isinstance(matcher, list):
            return obj in matcher  # Assuming `obj` can be directly compared to items in `matcher`
        else:
            # You might need additional type checks here to ensure `obj` and `matcher` are compatible
            return obj == matcher
    _filter = val_match
else:
    _filter = filter_lookup

In this solution, val_match assumes that obj can be compared directly to the items in matcher or to matcher itself. You might need to adjust this logic depending on the types T can represent, especially if T is not guaranteed to be comparable with the types in matcher.

General Note on Type Checking and Generics

When working with generics and type checking in Python, especially with complex class behaviors like QueryList, it's crucial to ensure that all type annotations accurately reflect the intended use and behaviors of your class methods. This includes being precise about the types of arguments, return values, and the handling of generic type variables throughout the class. Remember that mypy's type checking is based on the static analysis of these annotations, so discrepancies between the annotations and the actual code logic will lead to warnings or errors.

Copy link

codecov bot commented Feb 7, 2024

Codecov Report

Attention: 10 lines in your changes are missing coverage. Please review.

Comparison is base (32fff3e) 88.67% compared to head (cb9f465) 89.06%.

❗ Current head cb9f465 differs from pull request most recent head 76bcfc8. Consider uploading reports for the commit 76bcfc8 to get more accurate results

Files Patch % Lines
tests/_internal/test_query_list.py 69.69% 8 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #515      +/-   ##
==========================================
+ Coverage   88.67%   89.06%   +0.39%     
==========================================
  Files          35       36       +1     
  Lines        3522     3557      +35     
  Branches      488      502      +14     
==========================================
+ Hits         3123     3168      +45     
+ Misses        288      268      -20     
- Partials      111      121      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

src/libtmux/_internal/query_list.py:307: error: List comprehension has incompatible type List[tuple[Any, T]]; expected List[T]  [misc]
src/libtmux/_internal/query_list.py:376: error: Incompatible types in assignment (expression has type "Callable[[str | list[Any]], bool]", variable has type "Callable[[T], bool]")  [assignment]
tests/test_dataclasses.py:166: error: Incompatible types in assignment (expression has type "Window | None", variable has type "Window")  [assignment]
tests/test_dataclasses.py:171: error: Incompatible types in assignment (expression has type "Window | None", variable has type "Window")  [assignment]
@tony tony force-pushed the query-list-typings branch 2 times, most recently from 72f944e to cb9f465 Compare February 7, 2024 15:19
@tony tony changed the title QueryList: Fix typings fix!(QueryList): Generic fixes Feb 7, 2024
@tony tony merged commit da0aa24 into master Feb 7, 2024
22 checks passed
@tony tony deleted the query-list-typings branch February 7, 2024 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant