Skip to content

fix!(QueryList): Generic fixes #515

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

Merged
merged 8 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ $ pip install --user --upgrade --pre libtmux

<!-- Maintainers and contributors: Insert change notes for the next release above -->

### Improvement

- QueryList typings (#515)

- This improves the annotations in descendant objects such as:

- `Server.sessions`
- `Session.windows`
- `Window.panes`

- Bolster tests (ported from `libvcs`): doctests and pytests

## libtmux 0.26.0 (2024-02-06)

### Breaking change
Expand Down
203 changes: 185 additions & 18 deletions src/libtmux/_internal/query_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
import traceback
import typing as t
from collections.abc import Mapping, Sequence
from collections.abc import Iterable, Mapping, Sequence

if t.TYPE_CHECKING:

Expand All @@ -23,7 +23,7 @@ def __call__(
...


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

no_arg = object()

Expand All @@ -40,13 +40,55 @@ 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'].
"""Fetch values in objects and keys, supported nested data.

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

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

>>> keygetter({ "food": { "breakfast": "cereal" } }, "food__breakfast")
'cereal'

**With objects**:

>>> from typing import List, Optional
>>> from dataclasses import dataclass, field

>>> @dataclass()
... class Food:
... fruit: List[str] = field(default_factory=list)
... breakfast: Optional[str] = None


>>> @dataclass()
... class Restaurant:
... place: str
... city: str
... state: str
... food: Food = field(default_factory=Food)


>>> restaurant = Restaurant(
... place="Largo",
... city="Tampa",
... state="Florida",
... food=Food(
... fruit=["banana", "orange"], breakfast="cereal"
... )
... )

>>> restaurant
Restaurant(place='Largo',
city='Tampa',
state='Florida',
food=Food(fruit=['banana', 'orange'], breakfast='cereal'))

>>> keygetter(restaurant, "food")
Food(fruit=['banana', 'orange'], breakfast='cereal')

>>> keygetter(restaurant, "food__breakfast")
'cereal'
"""
try:
sub_fields = path.split("__")
Expand Down Expand Up @@ -74,10 +116,24 @@ def parse_lookup(

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'

It can also look up objects:

>>> from dataclasses import dataclass

>>> @dataclass()
... class Inventory:
... food: str

>>> item = Inventory(food="red apple")

>>> item
Inventory(food='red apple')

>>> parse_lookup(item, "food__istartswith", "__istartswith")
'red apple'
"""
try:
if isinstance(path, str) and isinstance(lookup, str) and path.endswith(lookup):
Expand Down Expand Up @@ -259,11 +315,13 @@ def __init__(self, op: str, *args: object):
return super().__init__(f"{op} not in LOOKUP_NAME_MAP")


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

*Experimental, unstable*.

**With dictionaries**:

>>> query = QueryList(
... [
... {
Expand All @@ -280,6 +338,7 @@ class QueryList(t.List[T]):
... },
... ]
... )

>>> query.filter(place="Chicago suburbs")[0]['city']
'Elmhurst'
>>> query.filter(place__icontains="chicago")[0]['city']
Expand All @@ -290,27 +349,135 @@ class QueryList(t.List[T]):
'Elmhurst'
>>> query.filter(foods__fruit__in="orange")[0]['city']
'Tampa'
>>> query.get(foods__fruit__in="orange")['city']

>>> query.filter(foods__fruit__in="apple")
[{'place': 'Chicago suburbs',
'city': 'Elmhurst',
'state': 'Illinois',
'foods':
{'fruit': ['apple', 'cantelope'], 'breakfast': 'waffles'}}]

>>> query.filter(foods__fruit__in="non_existent")
[]

**With objects**:

>>> from typing import Any, Dict
>>> from dataclasses import dataclass, field

>>> @dataclass()
... class Restaurant:
... place: str
... city: str
... state: str
... foods: Dict[str, Any]

>>> restaurant = Restaurant(
... place="Largo",
... city="Tampa",
... state="Florida",
... foods={
... "fruit": ["banana", "orange"], "breakfast": "cereal"
... }
... )

>>> restaurant
Restaurant(place='Largo',
city='Tampa',
state='Florida',
foods={'fruit': ['banana', 'orange'], 'breakfast': 'cereal'})

>>> query = QueryList([restaurant])

>>> query.filter(foods__fruit__in="banana")
[Restaurant(place='Largo',
city='Tampa',
state='Florida',
foods={'fruit': ['banana', 'orange'], 'breakfast': 'cereal'})]

>>> query.filter(foods__fruit__in="banana")[0].city
'Tampa'

>>> query.get(foods__fruit__in="banana").city
'Tampa'

**With objects (nested)**:

>>> from typing import List, Optional
>>> from dataclasses import dataclass, field

>>> @dataclass()
... class Food:
... fruit: List[str] = field(default_factory=list)
... breakfast: Optional[str] = None


>>> @dataclass()
... class Restaurant:
... place: str
... city: str
... state: str
... food: Food = field(default_factory=Food)


>>> query = QueryList([
... Restaurant(
... place="Largo",
... city="Tampa",
... state="Florida",
... food=Food(
... fruit=["banana", "orange"], breakfast="cereal"
... )
... ),
... Restaurant(
... place="Chicago suburbs",
... city="Elmhurst",
... state="Illinois",
... food=Food(
... fruit=["apple", "cantelope"], breakfast="waffles"
... )
... )
... ])

>>> query.filter(food__fruit__in="banana")
[Restaurant(place='Largo',
city='Tampa',
state='Florida',
food=Food(fruit=['banana', 'orange'], breakfast='cereal'))]

>>> query.filter(food__fruit__in="banana")[0].city
'Tampa'

>>> query.get(food__fruit__in="banana").city
'Tampa'

>>> query.filter(food__breakfast="waffles")
[Restaurant(place='Chicago suburbs',
city='Elmhurst',
state='Illinois',
food=Food(fruit=['apple', 'cantelope'], breakfast='waffles'))]

>>> query.filter(food__breakfast="waffles")[0].city
'Elmhurst'

>>> query.filter(food__breakfast="non_existent")
[]
"""

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

def items(self) -> t.List[T]:
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.Tuple[str, 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

Expand Down Expand Up @@ -363,7 +530,7 @@ def filter_lookup(obj: t.Any) -> bool:
_filter = matcher
elif matcher is not None:

def val_match(obj: t.Union[str, t.List[t.Any]]) -> bool:
def val_match(obj: t.Union[str, t.List[t.Any], T]) -> bool:
if isinstance(matcher, list):
return obj in matcher
else:
Expand Down
8 changes: 4 additions & 4 deletions src/libtmux/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def new_session(
# Relations
#
@property
def sessions(self) -> QueryList[Session]: # type:ignore
def sessions(self) -> QueryList[Session]:
"""Sessions contained in server.

Can be accessed via
Expand All @@ -512,7 +512,7 @@ def sessions(self) -> QueryList[Session]: # type:ignore
return QueryList(sessions)

@property
def windows(self) -> QueryList[Window]: # type:ignore
def windows(self) -> QueryList[Window]:
"""Windows contained in server's sessions.

Can be accessed via
Expand All @@ -531,7 +531,7 @@ def windows(self) -> QueryList[Window]: # type:ignore
return QueryList(windows)

@property
def panes(self) -> QueryList[Pane]: # type:ignore
def panes(self) -> QueryList[Pane]:
"""Panes contained in tmux server (across all windows in all sessions).

Can be accessed via
Expand Down Expand Up @@ -707,7 +707,7 @@ def list_sessions(self) -> t.List[Session]:
return self.sessions

@property
def children(self) -> QueryList["Session"]: # type:ignore
def children(self) -> QueryList["Session"]:
"""Was used by TmuxRelationalObject (but that's longer used in this class).

.. deprecated:: 0.16
Expand Down
6 changes: 3 additions & 3 deletions src/libtmux/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def from_session_id(cls, server: "Server", session_id: str) -> "Session":
# Relations
#
@property
def windows(self) -> QueryList["Window"]: # type:ignore
def windows(self) -> QueryList["Window"]:
"""Windows contained by session.

Can be accessed via
Expand All @@ -117,7 +117,7 @@ def windows(self) -> QueryList["Window"]: # type:ignore
return QueryList(windows)

@property
def panes(self) -> QueryList["Pane"]: # type:ignore
def panes(self) -> QueryList["Pane"]:
"""Panes contained by session's windows.

Can be accessed via
Expand Down Expand Up @@ -689,7 +689,7 @@ def list_windows(self) -> t.List["Window"]:
return self.windows

@property
def children(self) -> QueryList["Window"]: # type:ignore
def children(self) -> QueryList["Window"]:
"""Was used by TmuxRelationalObject (but that's longer used in this class).

.. deprecated:: 0.16
Expand Down
4 changes: 2 additions & 2 deletions src/libtmux/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def session(self) -> "Session":
return Session.from_session_id(server=self.server, session_id=self.session_id)

@property
def panes(self) -> QueryList["Pane"]: # type: ignore
def panes(self) -> QueryList["Pane"]:
"""Panes contained by window.

Can be accessed via
Expand Down Expand Up @@ -724,7 +724,7 @@ def list_panes(self) -> t.List["Pane"]:
return self.panes

@property
def children(self) -> QueryList["Pane"]: # type:ignore
def children(self) -> QueryList["Pane"]:
"""Was used by TmuxRelationalObject (but that's longer used in this class).

.. deprecated:: 0.16
Expand Down
Empty file added tests/_internal/__init__.py
Empty file.
Loading