Skip to content

Commit

Permalink
Add typing for cell arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
jieter committed Dec 23, 2024
1 parent ffed229 commit 555071a
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 58 deletions.
24 changes: 18 additions & 6 deletions django_tables2/columns/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections import OrderedDict
from collections.abc import Callable, Iterator
from typing import TYPE_CHECKING, Any, Optional, Union
from typing import TYPE_CHECKING, Any, Optional, TypedDict, Union

from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse
Expand All @@ -18,13 +18,23 @@
)

if TYPE_CHECKING:
from django.db.models import QuerySet
from django.db.models.fields import Field
from django.db.models import Field, QuerySet
from django.utils.safestring import SafeString
from typing_extensions import Unpack

from ..rows import BoundRow
from ..tables import Table


class CellArguments(TypedDict):
value: "Union[SafeString | QuerySet]"
record: Any
column: "Column"
bound_column: "BoundColumn"
bound_row: "BoundRow"
table: "Table"


class Library:
"""A collection of columns."""

Expand Down Expand Up @@ -363,7 +373,7 @@ def footer(self, bound_column: "BoundColumn", table: "Table") -> Optional[str]:

return ""

def render(self, value: Any, **kwargs) -> "SafeString":
def render(self, **kwargs: "Unpack[CellArguments]") -> "SafeString":
"""
Return the content for a specific cell.
Expand All @@ -375,9 +385,9 @@ def render(self, value: Any, **kwargs) -> "SafeString":
Subclasses should set `.empty_values` to ``()`` if they want to handle
all values in `.render`.
"""
return value
return kwargs["value"]

def value(self, **kwargs) -> Any:
def value(self, **kwargs: "Unpack[CellArguments]") -> Any:
"""
Return the content for a specific cell for exports.
Expand Down Expand Up @@ -446,6 +456,8 @@ class SimpleTable(tables.Table):
"""

_table: "Table"
column: Column
render: Callable
order: Callable
value: Callable
Expand Down
7 changes: 4 additions & 3 deletions django_tables2/columns/booleancolumn.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from django.utils.html import escape, format_html

from ..utils import AttributeDict
from .base import BoundColumn, Column, library
from .base import BoundColumn, CellArguments, Column, library

if TYPE_CHECKING:
from django.db.models import Field
from django.utils.safestring import SafeString
from typing_extensions import Unpack


@library.register
Expand Down Expand Up @@ -49,15 +50,15 @@ def _get_bool_value(self, record: Any, value: Any, bound_column: BoundColumn) ->

return bool(value)

def render(self, value: Any, **kwargs) -> "SafeString":
def render(self, **kwargs: "Unpack[CellArguments]") -> "SafeString":
value = self._get_bool_value(kwargs["record"], kwargs["value"], kwargs["bound_column"])
text = self.yesno[int(not value)]
attrs = {"class": str(value).lower()}
attrs.update(self.attrs.get("span", {}))

return format_html("<span {}>{}</span>", AttributeDict(attrs).as_html(), escape(text))

def value(self, **kwargs) -> str:
def value(self, **kwargs: "Unpack[CellArguments]") -> Any:
"""
Returns the content for a specific cell similarly to `.render` however without any html content.
"""
Expand Down
12 changes: 6 additions & 6 deletions django_tables2/columns/jsoncolumn.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import json
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Optional

from django.db.models import JSONField
from django.utils.html import format_html
from django.utils.safestring import SafeString

from ..utils import AttributeDict
from .base import Column, library
from .base import CellArguments, Column, library

if TYPE_CHECKING:
from django.db.models import Field
from typing_extensions import Unpack

try:
from django.contrib.postgres.fields import HStoreField
Expand Down Expand Up @@ -42,11 +43,10 @@ def __init__(self, json_dumps_kwargs=None, **kwargs):

super().__init__(**kwargs)

def render(self, value: Any, **kwargs) -> SafeString:
def render(self, **kwargs: "Unpack[CellArguments]") -> "SafeString":
attributes = AttributeDict(self.attrs.get("pre", {})).as_html()
return format_html(
"<pre {}>{}</pre>", attributes, json.dumps(value, **self.json_dumps_kwargs)
)
content = json.dumps(kwargs["value"], **self.json_dumps_kwargs)
return format_html("<pre {}>{}</pre>", attributes, content)

@classmethod
def from_field(cls, field: "Field", **kwargs) -> "Optional[JSONColumn]":
Expand Down
17 changes: 10 additions & 7 deletions django_tables2/columns/linkcolumn.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing import Any
from typing import TYPE_CHECKING, Any

from django.utils.safestring import SafeString

from .base import Column, library
from .base import CellArguments, Column, library

if TYPE_CHECKING:
from typing_extensions import Unpack


class BaseLinkColumn(Column):
Expand All @@ -28,15 +31,15 @@ def text_value(self, record, value) -> SafeString:
return value
return self.text(record) if callable(self.text) else self.text

def value(self, record, value):
def render(self, **kwargs: "Unpack[CellArguments]") -> SafeString:
return self.text_value(kwargs["record"], kwargs["value"])

def value(self, **kwargs: "Unpack[CellArguments]") -> Any:
"""
Returns the content for a specific cell similarly to `.render` however
without any html content.
"""
return self.text_value(record, value)

def render(self, value: Any, **kwargs) -> SafeString:
return self.text_value(kwargs["record"], value)
return self.text_value(kwargs["record"], kwargs["value"])


@library.register
Expand Down
11 changes: 7 additions & 4 deletions django_tables2/columns/manytomanycolumn.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from typing import Any, Optional
from typing import TYPE_CHECKING, Optional

from django.db import models
from django.utils.encoding import force_str
from django.utils.html import conditional_escape
from django.utils.safestring import SafeString, mark_safe

from .base import Column, LinkTransform, library
from .base import CellArguments, Column, LinkTransform, library

if TYPE_CHECKING:
from typing_extensions import Unpack


@library.register
Expand Down Expand Up @@ -86,9 +89,9 @@ def filter(self, qs: models.QuerySet):
"""
return qs.all()

def render(self, value: Any, **kwargs) -> "SafeString":
def render(self, **kwargs: "Unpack[CellArguments]") -> "SafeString":
items = []
for item in self.filter(value):
for item in self.filter(kwargs["value"]):
content = conditional_escape(self.transform(item))
if hasattr(self, "linkify_item"):
content = self.linkify_item(content=content, record=item)
Expand Down
17 changes: 11 additions & 6 deletions django_tables2/columns/templatecolumn.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional

from django.template import Context, Template
from django.template.loader import get_template
from django.utils.html import strip_tags
from django.utils.safestring import SafeString

from .base import BoundColumn, Column, library
from django_tables2.rows import BoundRow

from .base import BoundColumn, CellArguments, Column, library

if TYPE_CHECKING:
from typing_extensions import Unpack


@library.register
Expand Down Expand Up @@ -58,19 +63,19 @@ def __init__(
if not self.template_code and not self.template_name:
raise ValueError("A template must be provided")

def render(self, value: Any, **kwargs) -> "SafeString":
def render(self, **kwargs: "Unpack[CellArguments]") -> "SafeString":
# If the table is being rendered using `render_table`, it hackily
# attaches the context to the table as a gift to `TemplateColumn`.
table = kwargs["table"]
context = getattr(table, "context", Context())
bound_column: BoundColumn = kwargs["bound_column"]

bound_row: BoundRow = kwargs["bound_row"]
additional_context = {
"default": bound_column.default,
"column": bound_column,
"record": kwargs["record"],
"value": value,
"row_counter": kwargs["bound_row"].row_counter,
"value": kwargs["value"],
"row_counter": bound_row.row_counter,
}
additional_context.update(self.extra_context)
with context.update(additional_context):
Expand Down
49 changes: 24 additions & 25 deletions django_tables2/rows.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from typing import TYPE_CHECKING, Any

from django.core.exceptions import FieldDoesNotExist
from django.db import models

from .columns.base import BoundColumn, CellArguments
from .columns.linkcolumn import BaseLinkColumn
from .columns.manytomanycolumn import ManyToManyColumn
from .utils import A, AttributeDict, call_with_appropriate, computed_values

if TYPE_CHECKING:
from .tables import Table


class CellAccessor:
"""
Allows accessing cell contents on a row object (see `BoundRow`)
"""
"""Allows accessing cell contents on a row object (see `BoundRow`)."""

def __init__(self, row):
self.row = row
Expand Down Expand Up @@ -80,31 +84,26 @@ class BoundRow:
"""

def __init__(self, record, table):
def __init__(self, record: Any, table: "Table"):
self._record = record
self._table = table

self.row_counter = next(table._counter)

# support accessing cells from a template: {{ row.cells.column_name }}
# Support accessing cells from a template: {{ row.cells.column_name }}
self.cells = CellAccessor(self)

@property
def table(self):
def table(self) -> "Table":
"""The `.Table` this row is part of."""
return self._table

def get_even_odd_css_class(self):
"""
Return css class, alternating for odd and even records.
Return:
string: `even` for even records, `odd` otherwise.
"""
def get_even_odd_css_class(self) -> str:
"""Return "odd" and "even" depending on the row counter."""
return "odd" if self.row_counter % 2 else "even"

@property
def attrs(self):
def attrs(self) -> AttributeDict:
"""Return the attributes for a certain row."""
cssClass = self.get_even_odd_css_class()

Expand All @@ -120,7 +119,7 @@ def attrs(self):
return AttributeDict(row_attrs)

@property
def record(self):
def record(self) -> Any:
"""The data record from the data source which is used to populate this row with data."""
return self._record

Expand All @@ -136,7 +135,7 @@ def __iter__(self):
# is correct – it's what __getitem__ expects.
yield value

def _get_and_render_with(self, bound_column, render_func, default):
def _get_and_render_with(self, bound_column: BoundColumn, render_func, default):
value = None
accessor = A(bound_column.accessor)
column = bound_column.column
Expand Down Expand Up @@ -172,20 +171,20 @@ def _get_and_render_with(self, bound_column, render_func, default):

return render_func(bound_column, value)

def _optional_cell_arguments(self, bound_column, value):
def _optional_cell_arguments(self, bound_column: "BoundRow", value: Any) -> CellArguments:
"""
Defines the arguments that will optionally be passed while calling the
cell's rendering or value getter if that function has one of these as a
keyword argument.
"""
return {
"value": value,
"record": self.record,
"column": bound_column.column,
"bound_column": bound_column,
"bound_row": self,
"table": self._table,
}
return CellArguments(
value=value,
record=self.record,
column=bound_column.column,
bound_column=bound_column,
bound_row=self,
table=self._table,
)

def get_cell(self, name):
"""
Expand Down
2 changes: 1 addition & 1 deletion django_tables2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ def bits(self):

def get_field(
self, model: models.Model
) -> Union[models.Field[Any, Any], models.ForeignObjectRel, "GenericForeignKey", None]:
) -> "Union[models.Field[Any, Any], models.ForeignObjectRel, GenericForeignKey, None]":
"""
Return the django model field for model in context, following relations.
"""
Expand Down

0 comments on commit 555071a

Please sign in to comment.