Skip to content

Commit ccd8321

Browse files
committed
Revert "fix(query_list): improve object identity handling in comparisons"
This reverts commit 8a4c66b.
1 parent 7835d65 commit ccd8321

File tree

1 file changed

+142
-154
lines changed

1 file changed

+142
-154
lines changed

src/libtmux/_internal/query_list.py

+142-154
Original file line numberDiff line numberDiff line change
@@ -42,50 +42,79 @@ class ObjectDoesNotExist(Exception):
4242
"""The requested object does not exist."""
4343

4444

45-
def keygetter(obj: t.Any, path: str | None) -> t.Any:
46-
"""Get a value from an object using a path string.
45+
def keygetter(
46+
obj: Mapping[str, t.Any],
47+
path: str,
48+
) -> None | t.Any | str | list[str] | Mapping[str, str]:
49+
"""Fetch values in objects and keys, supported nested data.
4750
48-
Args:
49-
obj: The object to get the value from
50-
path: The path to the value, using double underscores as separators
51+
**With dictionaries**:
5152
52-
Returns
53-
-------
54-
The value at the path, or None if the path is invalid
55-
"""
56-
if not isinstance(path, str):
57-
return None
53+
>>> keygetter({ "food": { "breakfast": "cereal" } }, "food")
54+
{'breakfast': 'cereal'}
5855
59-
if not path or path == "__":
60-
if hasattr(obj, "__dict__"):
61-
return obj
62-
return None
56+
>>> keygetter({ "food": { "breakfast": "cereal" } }, "food__breakfast")
57+
'cereal'
58+
59+
**With objects**:
60+
61+
>>> from typing import List, Optional
62+
>>> from dataclasses import dataclass, field
63+
64+
>>> @dataclass()
65+
... class Food:
66+
... fruit: List[str] = field(default_factory=list)
67+
... breakfast: Optional[str] = None
68+
69+
70+
>>> @dataclass()
71+
... class Restaurant:
72+
... place: str
73+
... city: str
74+
... state: str
75+
... food: Food = field(default_factory=Food)
76+
77+
78+
>>> restaurant = Restaurant(
79+
... place="Largo",
80+
... city="Tampa",
81+
... state="Florida",
82+
... food=Food(
83+
... fruit=["banana", "orange"], breakfast="cereal"
84+
... )
85+
... )
86+
87+
>>> restaurant
88+
Restaurant(place='Largo',
89+
city='Tampa',
90+
state='Florida',
91+
food=Food(fruit=['banana', 'orange'], breakfast='cereal'))
6392
64-
if not isinstance(obj, (dict, Mapping)) and not hasattr(obj, "__dict__"):
65-
return obj
93+
>>> keygetter(restaurant, "food")
94+
Food(fruit=['banana', 'orange'], breakfast='cereal')
6695
96+
>>> keygetter(restaurant, "food__breakfast")
97+
'cereal'
98+
"""
6799
try:
68-
parts = path.split("__")
69-
current = obj
70-
for part in parts:
71-
if not part:
72-
continue
73-
if isinstance(current, (dict, Mapping)):
74-
if part not in current:
75-
return None
76-
current = current[part]
77-
elif hasattr(current, part):
78-
current = getattr(current, part)
79-
else:
80-
return None
81-
return current
100+
sub_fields = path.split("__")
101+
dct = obj
102+
for sub_field in sub_fields:
103+
if isinstance(dct, dict):
104+
dct = dct[sub_field]
105+
elif hasattr(dct, sub_field):
106+
dct = getattr(dct, sub_field)
107+
82108
except Exception as e:
83-
logger.debug(f"Error in keygetter: {e}")
109+
traceback.print_stack()
110+
logger.debug(f"The above error was {e}")
84111
return None
85112

113+
return dct
114+
86115

87116
def parse_lookup(
88-
obj: Mapping[str, t.Any] | t.Any,
117+
obj: Mapping[str, t.Any],
89118
path: str,
90119
lookup: str,
91120
) -> t.Any | None:
@@ -114,8 +143,8 @@ def parse_lookup(
114143
"""
115144
try:
116145
if isinstance(path, str) and isinstance(lookup, str) and path.endswith(lookup):
117-
field_name = path.rsplit(lookup, 1)[0]
118-
if field_name:
146+
field_name = path.rsplit(lookup)[0]
147+
if field_name is not None:
119148
return keygetter(obj, field_name)
120149
except Exception as e:
121150
traceback.print_stack()
@@ -161,8 +190,7 @@ def lookup_icontains(
161190
return rhs.lower() in data.lower()
162191
if isinstance(data, Mapping):
163192
return rhs.lower() in [k.lower() for k in data]
164-
if isinstance(data, list):
165-
return any(rhs.lower() in str(item).lower() for item in data)
193+
166194
return False
167195

168196

@@ -212,11 +240,18 @@ def lookup_in(
212240
if isinstance(rhs, list):
213241
return data in rhs
214242

215-
if isinstance(rhs, str) and isinstance(data, Mapping):
216-
return rhs in data
217-
if isinstance(rhs, str) and isinstance(data, (str, list)):
218-
return rhs in data
219-
# TODO: Add a deep dictionary matcher
243+
try:
244+
if isinstance(rhs, str) and isinstance(data, Mapping):
245+
return rhs in data
246+
if isinstance(rhs, str) and isinstance(data, (str, list)):
247+
return rhs in data
248+
if isinstance(rhs, str) and isinstance(data, Mapping):
249+
return rhs in data
250+
# TODO: Add a deep Mappingionary matcher
251+
# if isinstance(rhs, Mapping) and isinstance(data, Mapping):
252+
# return rhs.items() not in data.items()
253+
except Exception:
254+
return False
220255
return False
221256

222257

@@ -227,11 +262,18 @@ def lookup_nin(
227262
if isinstance(rhs, list):
228263
return data not in rhs
229264

230-
if isinstance(rhs, str) and isinstance(data, Mapping):
231-
return rhs not in data
232-
if isinstance(rhs, str) and isinstance(data, (str, list)):
233-
return rhs not in data
234-
# TODO: Add a deep dictionary matcher
265+
try:
266+
if isinstance(rhs, str) and isinstance(data, Mapping):
267+
return rhs not in data
268+
if isinstance(rhs, str) and isinstance(data, (str, list)):
269+
return rhs not in data
270+
if isinstance(rhs, str) and isinstance(data, Mapping):
271+
return rhs not in data
272+
# TODO: Add a deep Mappingionary matcher
273+
# if isinstance(rhs, Mapping) and isinstance(data, Mapping):
274+
# return rhs.items() not in data.items()
275+
except Exception:
276+
return False
235277
return False
236278

237279

@@ -272,39 +314,12 @@ def lookup_iregex(
272314

273315
class PKRequiredException(Exception):
274316
def __init__(self, *args: object) -> None:
275-
super().__init__("items() require a pk_key exists")
317+
return super().__init__("items() require a pk_key exists")
276318

277319

278320
class OpNotFound(ValueError):
279321
def __init__(self, op: str, *args: object) -> None:
280-
super().__init__(f"{op} not in LOOKUP_NAME_MAP")
281-
282-
283-
def _compare_values(a: t.Any, b: t.Any) -> bool:
284-
"""Helper function to compare values with numeric tolerance."""
285-
if a is b:
286-
return True
287-
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
288-
return abs(a - b) <= 1
289-
if isinstance(a, Mapping) and isinstance(b, Mapping):
290-
if a.keys() != b.keys():
291-
return False
292-
for key in a.keys():
293-
if not _compare_values(a[key], b[key]):
294-
return False
295-
return True
296-
if hasattr(a, "__eq__") and not isinstance(a, (str, int, float, bool, list, dict)):
297-
# For objects with custom equality
298-
return bool(a == b)
299-
if (
300-
isinstance(a, object)
301-
and isinstance(b, object)
302-
and type(a) is object
303-
and type(b) is object
304-
):
305-
# For objects that don't define equality, consider them equal if they are both bare objects
306-
return True
307-
return a == b
322+
return super().__init__(f"{op} not in LOOKUP_NAME_MAP")
308323

309324

310325
class QueryList(list[T], t.Generic[T]):
@@ -457,98 +472,80 @@ class QueryList(list[T], t.Generic[T]):
457472
"""
458473

459474
data: Sequence[T]
460-
pk_key: str | None = None
475+
pk_key: str | None
461476

462477
def __init__(self, items: Iterable[T] | None = None) -> None:
463478
super().__init__(items if items is not None else [])
464479

465480
def items(self) -> list[tuple[str, T]]:
466481
if self.pk_key is None:
467482
raise PKRequiredException
468-
return [(str(getattr(item, self.pk_key)), item) for item in self]
483+
return [(getattr(item, self.pk_key), item) for item in self]
469484

470-
def __eq__(self, other: object) -> bool:
471-
if not isinstance(other, list):
472-
return False
485+
def __eq__(
486+
self,
487+
other: object,
488+
) -> bool:
489+
data = other
473490

474-
if len(self) != len(other):
491+
if not isinstance(self, list) or not isinstance(data, list):
475492
return False
476493

477-
for a, b in zip(self, other):
478-
if a is b:
479-
continue
480-
if isinstance(a, Mapping) and isinstance(b, Mapping):
481-
if a.keys() != b.keys():
482-
return False
483-
for key in a.keys():
484-
if (
485-
key == "banana"
486-
and isinstance(a[key], object)
487-
and isinstance(b[key], object)
488-
and type(a[key]) is object
489-
and type(b[key]) is object
490-
):
491-
# Special case for bare object() instances in the test
492-
continue
493-
if not _compare_values(a[key], b[key]):
494-
return False
495-
else:
496-
if not _compare_values(a, b):
494+
if len(self) == len(data):
495+
for a, b in zip(self, data):
496+
if isinstance(a, Mapping):
497+
a_keys = a.keys()
498+
if a.keys == b.keys():
499+
for key in a_keys:
500+
if abs(a[key] - b[key]) > 1:
501+
return False
502+
elif a != b:
497503
return False
498-
return True
504+
505+
return True
506+
return False
499507

500508
def filter(
501509
self,
502510
matcher: Callable[[T], bool] | T | None = None,
503-
**lookups: t.Any,
511+
**kwargs: t.Any,
504512
) -> QueryList[T]:
505-
"""Filter list of objects.
506-
507-
Args:
508-
matcher: Optional callable or value to match against
509-
**lookups: The lookup parameters to filter by
513+
"""Filter list of objects."""
510514

511-
Returns
512-
-------
513-
A new QueryList containing only the items that match
514-
"""
515-
if matcher is not None:
516-
if callable(matcher):
517-
return self.__class__([item for item in self if matcher(item)])
518-
elif isinstance(matcher, list):
519-
return self.__class__([item for item in self if item in matcher])
520-
else:
521-
return self.__class__([item for item in self if item == matcher])
522-
523-
if not lookups:
524-
# Return a new QueryList with the exact same items
525-
# We need to use list(self) to preserve object identity
526-
return self.__class__(self)
527-
528-
result = []
529-
for item in self:
530-
matches = True
531-
for key, value in lookups.items():
515+
def filter_lookup(obj: t.Any) -> bool:
516+
for path, v in kwargs.items():
532517
try:
533-
path, op = key.rsplit("__", 1)
518+
lhs, op = path.rsplit("__", 1)
519+
534520
if op not in LOOKUP_NAME_MAP:
535-
path = key
536-
op = "exact"
521+
raise OpNotFound(op=op)
537522
except ValueError:
538-
path = key
523+
lhs = path
539524
op = "exact"
540525

541-
item_value = keygetter(item, path)
542-
lookup_fn = LOOKUP_NAME_MAP[op]
543-
if not lookup_fn(item_value, value):
544-
matches = False
545-
break
526+
assert op in LOOKUP_NAME_MAP
527+
path = lhs
528+
data = keygetter(obj, path)
546529

547-
if matches:
548-
# Preserve the exact item reference
549-
result.append(item)
530+
if data is None or not LOOKUP_NAME_MAP[op](data, v):
531+
return False
532+
533+
return True
534+
535+
if callable(matcher):
536+
filter_ = matcher
537+
elif matcher is not None:
538+
539+
def val_match(obj: str | list[t.Any] | T) -> bool:
540+
if isinstance(matcher, list):
541+
return obj in matcher
542+
return bool(obj == matcher)
550543

551-
return self.__class__(result)
544+
filter_ = val_match
545+
else:
546+
filter_ = filter_lookup
547+
548+
return self.__class__(k for k in self if filter_(k))
552549

553550
def get(
554551
self,
@@ -560,18 +557,9 @@ def get(
560557
561558
Raises :exc:`MultipleObjectsReturned` if multiple objects found.
562559
563-
Raises :exc:`ObjectDoesNotExist` if no object found, unless ``default`` is given.
560+
Raises :exc:`ObjectDoesNotExist` if no object found, unless ``default`` stated.
564561
"""
565-
if matcher is not None:
566-
if callable(matcher):
567-
objs = [item for item in self if matcher(item)]
568-
elif isinstance(matcher, list):
569-
objs = [item for item in self if item in matcher]
570-
else:
571-
objs = [item for item in self if item == matcher]
572-
else:
573-
objs = self.filter(**kwargs)
574-
562+
objs = self.filter(matcher=matcher, **kwargs)
575563
if len(objs) > 1:
576564
raise MultipleObjectsReturned
577565
if len(objs) == 0:

0 commit comments

Comments
 (0)