Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ Unreleased
- Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``.
:pr:`1793`
- Use ``flit_core`` instead of ``setuptools`` as build backend.
- The ``map`` filter (with ``attribute=...``) and ``groupby`` filter
accept an explicit ``none`` value for ``default``. Previously,
``default=none`` was indistinguishable from omitting ``default``, so
missing attributes and keys were not replaced with ``None``.
:issue:`2165`


Version 3.1.6
Expand Down
18 changes: 11 additions & 7 deletions src/jinja2/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .exceptions import FilterArgumentError
from .runtime import Undefined
from .utils import htmlsafe_json_dumps
from .utils import missing
from .utils import pass_context
from .utils import pass_environment
from .utils import pass_eval_context
Expand Down Expand Up @@ -59,7 +60,7 @@ def make_attrgetter(
environment: "Environment",
attribute: str | int | None,
postprocess: t.Callable[[t.Any], t.Any] | None = None,
default: t.Any | None = None,
default: t.Any | None = missing,
) -> t.Callable[[t.Any], t.Any]:
"""Returns a callable that looks up the given attribute from a
passed object with the rules of the environment. Dots are allowed
Expand All @@ -72,7 +73,7 @@ def attrgetter(item: t.Any) -> t.Any:
for part in parts:
item = environment.getitem(item, part)

if default is not None and isinstance(item, Undefined):
if default is not missing and isinstance(item, Undefined):
item = default

if postprocess is not None:
Expand Down Expand Up @@ -1199,7 +1200,7 @@ def sync_do_groupby(
environment: "Environment",
value: "t.Iterable[V]",
attribute: str | int,
default: t.Any | None = None,
default: t.Any | None = missing,
case_sensitive: bool = False,
) -> "list[_GroupTuple]":
"""Group a sequence of objects by an attribute using Python's
Expand Down Expand Up @@ -1283,7 +1284,7 @@ async def do_groupby(
environment: "Environment",
value: "t.AsyncIterable[V] | t.Iterable[V]",
attribute: str | int,
default: t.Any | None = None,
default: t.Any | None = missing,
case_sensitive: bool = False,
) -> "list[_GroupTuple]":
expr = make_attrgetter(
Expand Down Expand Up @@ -1443,7 +1444,7 @@ def sync_do_map(
value: t.Iterable[t.Any],
*,
attribute: str = ...,
default: t.Any | None = None,
default: t.Any | None = missing,
) -> t.Iterable[t.Any]: ...


Expand Down Expand Up @@ -1513,7 +1514,7 @@ def do_map(
value: t.AsyncIterable[t.Any] | t.Iterable[t.Any],
*,
attribute: str = ...,
default: t.Any | None = None,
default: t.Any | None = missing,
) -> t.Iterable[t.Any]: ...


Expand Down Expand Up @@ -1720,7 +1721,10 @@ def prepare_map(
) -> t.Callable[[t.Any], t.Any]:
if not args and "attribute" in kwargs:
attribute = kwargs.pop("attribute")
default = kwargs.pop("default", None)
if "default" in kwargs:
default = kwargs.pop("default")
else:
default = missing

if kwargs:
raise FilterArgumentError(
Expand Down
21 changes: 21 additions & 0 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from jinja2 import TemplateRuntimeError
from jinja2 import UndefinedError
from jinja2.exceptions import TemplateAssertionError
from jinja2.nativetypes import NativeEnvironment
from jinja2.runtime import Undefined


class Magic:
Expand Down Expand Up @@ -741,6 +743,25 @@ def test_map_default(self, env):
assert test_list.render(users=users) == "lennon, edwards, None, ['smith', 'x']"
assert test_str.render(users=users) == "lennon, edwards, None, "

def test_map_explicit_none_default(self):
env = NativeEnvironment()
tmpl = env.from_string('{{ [{}]|map(attribute="foo", default=none)|list }}')
assert tmpl.render() == [None]

def test_map_omitted_default_remains_undefined(self):
env = NativeEnvironment()
tmpl = env.from_string('{{ [{}]|map(attribute="foo")|list }}')
items = tmpl.render()
assert len(items) == 1
assert isinstance(items[0], Undefined)

def test_groupby_explicit_none_default(self):
env = NativeEnvironment()
tmpl = env.from_string(
'{{ [{}]|groupby("k", default=none)|map(attribute="grouper")|first }}'
)
assert tmpl.render() is None

def test_simple_select(self, env):
env = Environment()
tmpl = env.from_string('{{ [1, 2, 3, 4, 5]|select("odd")|join("|") }}')
Expand Down
Loading