Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/queryish.egg-info
/dist
/build
/.coverage
__pycache__
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ x.x (Unreleased)

* Add `get_field` and `get_fields` methods to VirtualModel meta class (smark-1)
* Add dedicated `DoesNotExist` and `MultipleObjectsReturned` exceptions on VirtualModel
* Make properties `start` and `stop` available in addition to `offset` and `limit`
* Short-circuit queries if running on an empty slice
* Fix: When retrieving by index, use a single-item slice instead of running the full query


0.2 (2023-09-05)
Expand Down
85 changes: 47 additions & 38 deletions queryish/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ class Queryish:
def __init__(self):
self._results = None
self._count = None
self.offset = 0
self.limit = None
self.start = 0
self.stop = None
self.filters = []
self.filter_fields = None
self.ordering = ()
self.ordering_fields = None

@property
def offset(self):
return self.start

@property
def limit(self):
if self.stop is None:
return None
return self.stop - self.start

def run_query(self):
raise NotImplementedError

Expand All @@ -35,7 +45,10 @@ def run_count(self):

def __iter__(self):
if self._results is None:
results = self.run_query()
if self.start == self.stop:
results = []
else:
results = self.run_query()
if isinstance(results, list):
self._results = results
for result in results:
Expand All @@ -53,14 +66,19 @@ def count(self):
if self._count is None:
if self._results is not None:
self._count = len(self._results)
elif self.start == self.stop:
self._count = 0
else:
self._count = self.run_count()
return self._count

def __len__(self):
# __len__ must run the full query
if self._results is None:
self._results = list(self.run_query())
if self.start == self.stop:
self._results = []
else:
self._results = list(self.run_query())
return len(self._results)

def clone(self, **kwargs):
Expand Down Expand Up @@ -123,46 +141,40 @@ def all(self):
def ordered(self):
return bool(self.ordering)

def __getitem__(self, key):
if isinstance(key, slice):
if key.step is not None:
raise ValueError("%r does not support slicing with a step" % self.__class__.__name__)

# Adjust the requested start/stop values to be relative to the full queryset
absolute_start = (key.start or 0) + self.offset
if key.stop is None:
absolute_stop = None
else:
absolute_stop = key.stop + self.offset
def set_limits(self, start, stop):
if (start is not None and start < 0) or (stop is not None and stop < 0):
raise ValueError("Negative indexing is not supported")

# find the absolute stop value corresponding to the current limit
if self.limit is None:
current_absolute_stop = None
if stop is not None:
if self.stop is not None:
self.stop = min(self.stop, self.start + stop)
else:
current_absolute_stop = self.offset + self.limit

if absolute_stop is None:
final_absolute_stop = current_absolute_stop
elif current_absolute_stop is None:
final_absolute_stop = absolute_stop
self.stop = self.start + stop
if start is not None:
if self.stop is not None:
self.start = min(self.stop, self.start + start)
else:
final_absolute_stop = min(current_absolute_stop, absolute_stop)
self.start = self.start + start

if final_absolute_stop is None:
new_limit = None
else:
new_limit = final_absolute_stop - absolute_start
def __getitem__(self, key):
if isinstance(key, slice):
if key.step is not None:
raise ValueError("%r does not support slicing with a step" % self.__class__.__name__)

clone = self.clone(offset=absolute_start, limit=new_limit)
clone = self.clone()
clone.set_limits(key.start, key.stop)
if self._results:
clone._results = self._results[key]
return clone
elif isinstance(key, int):
if key < 0:
raise IndexError("Negative indexing is not supported")
if self._results is None:
self._results = list(self.run_query())
return self._results[key]
if self._results is not None:
return self._results[key]
else:
clone = self.clone()
clone.set_limits(key, key + 1)
return list(clone)[0]
else:
raise TypeError(
"%r indices must be integers or slices, not %s"
Expand All @@ -188,9 +200,7 @@ def __init__(self, model_name, fields, verbose_name, verbose_name_plural):
self.private_fields = []
self.concrete_fields = self.fields
self.many_to_many = []



def get_fields(self, include_parents=True, include_hidden=False):
"""
Return a list of fields associated to the model. By default, include
Expand All @@ -201,13 +211,12 @@ def get_fields(self, include_parents=True, include_hidden=False):
- include_hidden: include fields that have a related_name that
starts with a "+"
"""

fields = []

for field in self.fields:
fields.append(self.get_field(field))
return tuple(fields)

def get_field(self,field_name):
try:
from django.db import models
Expand All @@ -224,10 +233,10 @@ def get_field(self,field_name):
except ImportError:
raise ImportError("django must be installed to use get_field")


def get_parent_list(self):
return []


class VirtualModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# Create custom exception classes for DoesNotExist and MultipleObjectsReturned
Expand Down
Loading