Skip to content

Commit ff13428

Browse files
committed
Add typing
1 parent d55d53d commit ff13428

File tree

3 files changed

+81
-21
lines changed

3 files changed

+81
-21
lines changed

src/webstack_django_sorting/common.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@
33
"""
44

55
from operator import attrgetter
6+
from typing import Any, Literal, Sequence
67

7-
from django.db.models import F
8-
from django.http import Http404
8+
from django.db.models import F, QuerySet
9+
from django.http import Http404, HttpRequest
910

1011
from . import settings
1112

1213

13-
def render_sort_anchor(request, field_name, title, default_direction):
14+
def render_sort_anchor(
15+
request: HttpRequest,
16+
field_name: str,
17+
title: str,
18+
default_direction: Literal["asc", "desc"],
19+
) -> str:
20+
"""Render an HTML anchor tag for sorting a column."""
1421
get_params = request.GET.copy()
1522
sort_by = get_params.get("sort", None)
1623
if sort_by == field_name:
@@ -41,9 +48,9 @@ def render_sort_anchor(request, field_name, title, default_direction):
4148
return f'<a href="{request.path}{url_append}" title="{title}">{title}{icon}</a>'
4249

4350

44-
def get_order_by_from_request(request):
51+
def get_order_by_from_request(request: HttpRequest) -> str:
4552
"""
46-
Retrieve field used for sorting a queryset
53+
Retrieve field used for sorting a queryset.
4754
4855
:param request: HTTP request
4956
:return: the sorted field name, prefixed with "-" if ordering is descending
@@ -54,7 +61,8 @@ def get_order_by_from_request(request):
5461
return f"{sort_sign}{field_name}"
5562

5663

57-
def need_python_sorting(queryset, order_by):
64+
def need_python_sorting(queryset: QuerySet[Any], order_by: str) -> bool:
65+
"""Check if Python sorting is needed (for non-database fields)."""
5866
if order_by.find("__") >= 0:
5967
# Python can't sort order_by with '__'
6068
return False
@@ -65,9 +73,19 @@ def need_python_sorting(queryset, order_by):
6573
return field not in field_names
6674

6775

68-
def sort_queryset(queryset, order_by, null_ordering):
69-
"""order_by is an Django ORM order_by argument"""
76+
def sort_queryset(
77+
queryset: QuerySet[Any],
78+
order_by: str,
79+
null_ordering: dict[str, bool],
80+
) -> QuerySet[Any] | Sequence[Any]:
81+
"""
82+
Sort a queryset by the given field.
7083
84+
:param queryset: The queryset to sort
85+
:param order_by: Django ORM order_by argument (e.g., "name" or "-name")
86+
:param null_ordering: Dict with nulls_first or nulls_last setting
87+
:return: Sorted queryset or list (if Python sorting is used)
88+
"""
7189
if not order_by:
7290
# In this case the queryset can't be ordered (no field name specified)
7391
# even though nulls ordering is set
@@ -79,7 +97,7 @@ def sort_queryset(queryset, order_by, null_ordering):
7997
if order_by[0] == "-":
8098
if len(order_by) == 1:
8199
# Prefix without field name
82-
raise ValueError
100+
raise ValueError("Order by prefix without field name")
83101

84102
reverse = True
85103
name = order_by[1:]
@@ -91,13 +109,23 @@ def sort_queryset(queryset, order_by, null_ordering):
91109
# Fallback on pure Python sorting (much slower on large data)
92110
if hasattr(queryset[0], name):
93111
return sorted(queryset, key=attrgetter(name), reverse=reverse)
94-
raise AttributeError
112+
raise AttributeError(f"Object has no attribute '{name}'")
95113

96114
ordering_exp = (F(name).desc if reverse else F(name).asc)(**null_ordering)
97115
return queryset.order_by(ordering_exp)
98116

99117

100-
def get_null_ordering(request, default_template_ordering=None):
118+
def get_null_ordering(
119+
request: HttpRequest,
120+
default_template_ordering: str | None = None,
121+
) -> dict[str, bool]:
122+
"""
123+
Get null ordering configuration from request or template default.
124+
125+
:param request: HTTP request
126+
:param default_template_ordering: Default ordering from template ("first" or "last")
127+
:return: Dict with nulls_first or nulls_last setting, or empty dict
128+
"""
101129
# Prioritize changes in URL parameter over the default template variable
102130
nulls_value = request.GET.get("nulls", default_template_ordering)
103131
if nulls_value:
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
1+
from typing import Any, Literal, Sequence
2+
3+
from django.db.models import QuerySet
4+
from django.http import HttpRequest
15
from markupsafe import Markup
26

37
from . import common
48

59

6-
def sorting_anchor(request, field_name, title, default_sort_order="asc"):
10+
def sorting_anchor(
11+
request: HttpRequest,
12+
field_name: str,
13+
title: str,
14+
default_sort_order: Literal["asc", "desc"] = "asc",
15+
) -> Markup:
16+
"""Render a sorting anchor link for Jinja2 templates."""
717
return Markup(common.render_sort_anchor(request, field_name, title, default_sort_order))
818

919

10-
def sort_queryset(request, queryset, **context_var):
20+
def sort_queryset(
21+
request: HttpRequest,
22+
queryset: QuerySet[Any],
23+
**context_var: str | None,
24+
) -> QuerySet[Any] | Sequence[Any]:
25+
"""Sort a queryset based on request parameters for Jinja2 templates."""
1126
order_by = common.get_order_by_from_request(request)
1227
null_ordering = common.get_null_ordering(request, context_var.get("nulls"))
1328
return common.sort_queryset(queryset, order_by, null_ordering)

src/webstack_django_sorting/templatetags/sorting_tags.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
from typing import Literal
2+
13
from django import template
24
from django.http import Http404
5+
from django.template import Context
6+
from django.template.base import Parser, Token
37
from django.utils.translation import gettext_lazy as _
48

59
from .. import common, settings
610

711
register = template.Library()
812

913

10-
def anchor(parser, token):
14+
def anchor(parser: Parser, token: Token) -> SortAnchorNode:
1115
"""
1216
Parses a tag that's supposed to be in this format '{% anchor field title %}'
1317
Title may be a "string", _("trans string"), or variable
@@ -59,14 +63,21 @@ class SortAnchorNode(template.Node):
5963
6064
"""
6165

62-
def __init__(self, field, title, title_is_var, title_is_translatable, default_sort_order):
66+
def __init__(
67+
self,
68+
field: str,
69+
title: str,
70+
title_is_var: bool,
71+
title_is_translatable: bool,
72+
default_sort_order: Literal["asc", "desc"],
73+
) -> None:
6374
self.field = field
6475
self.title = title
6576
self.title_is_var = title_is_var
6677
self.title_is_translatable = title_is_translatable
6778
self.default_sort_order = default_sort_order
6879

69-
def render(self, context):
80+
def render(self, context: Context) -> str:
7081
if self.title_is_var:
7182
display_title = context[self.title]
7283
elif self.title_is_translatable:
@@ -79,17 +90,18 @@ def render(self, context):
7990
)
8091

8192

82-
def autosort(parser, token):
93+
def autosort(parser: Parser, token: Token) -> SortedDataNode:
94+
"""Parse the autosort template tag."""
8395
bits = [b.strip("\"'") for b in token.split_contents()]
8496
help_msg = "autosort tag synopsis: {%% autosort queryset [as context_variable] %%}"
85-
context_var = None
97+
context_var: str | None = None
8698

8799
# Check if their is some optional parameter (as new_context_var, nulls)
88100
if 2 > len(bits) > 7:
89101
raise template.TemplateSyntaxError(help_msg)
90102

91103
context_var = None
92-
null_ordering = None
104+
null_ordering: str | None = None
93105

94106
for index, bit in enumerate(bits):
95107
if index > 1:
@@ -107,12 +119,17 @@ class SortedDataNode(template.Node):
107119
Automatically sort a queryset with {% autosort queryset %}
108120
"""
109121

110-
def __init__(self, queryset_var, null_ordering, context_var=None):
122+
def __init__(
123+
self,
124+
queryset_var: str,
125+
null_ordering: str | None,
126+
context_var: str | None = None,
127+
) -> None:
111128
self.queryset_var = template.Variable(queryset_var)
112129
self.context_var = context_var
113130
self.null_ordering = null_ordering
114131

115-
def render(self, context):
132+
def render(self, context: Context) -> str:
116133
if self.context_var is not None:
117134
key = self.context_var
118135
else:

0 commit comments

Comments
 (0)