fix(template): support boolean argument in Jinja default filter#292
Conversation
bbc45fb to
d50d7ff
Compare
Conductor overrides Jinja's built-in `default` filter with a two-argument
implementation, so templates using the standard third `boolean` argument
failed with TypeError: _default_filter() takes from 1 to 2 positional
arguments but 3 were given.
Changes:
- _default_filter now accepts boolean: bool = False
- boolean=False (default): preserves current Conductor behavior —
only None maps to the default; "", 0, False, [], {} pass through as-is
- boolean=True: standard Jinja falsey semantics — any falsey value maps
to the default; truthy values pass through unchanged
- The `default` parameter name is kept (not renamed to `default_value`)
so existing callers passing it as a keyword argument keep working
Tests (14 new under TestTemplateRendererDefaultFilter):
- boolean=True with each falsey value returns the default
- boolean=True with a truthy value returns the value unchanged
- Two-argument form: None maps to default; "", 0, False, [], {} pass through
- Explicit boolean=False for the same edge cases
Out of scope: Jinja Undefined handling is unchanged — Conductor runs with
StrictUndefined, so undefined variables raise TemplateError before they
ever reach the filter (fail-fast preserved).
Fixes microsoft#288
d50d7ff to
005b606
Compare
jrob5756
left a comment
There was a problem hiding this comment.
Good change. Some small things, the new argument works for defined falsey values, but the missing-variable path still differs from Jinja and needs a regression test. I also left a small docstring clarification.
If you can wrap these up, we can merge! Thanks again.
| if boolean: | ||
| if not value: | ||
| return default | ||
| else: | ||
| if value is None: | ||
| return default | ||
| return value |
There was a problem hiding this comment.
This still breaks the missing-value case from #288. With StrictUndefined, not value calls StrictUndefined.__bool__ and raises, while Jinja's built-in default returns the fallback. Please check Undefined before testing truthiness and add coverage for a missing variable in both forms. This replacement also needs Undefined imported from jinja2.
| if boolean: | |
| if not value: | |
| return default | |
| else: | |
| if value is None: | |
| return default | |
| return value | |
| if isinstance(value, Undefined): | |
| return default | |
| if boolean and not value: | |
| return default | |
| if value is None: | |
| return default | |
| return value |
There was a problem hiding this comment.
Fixed in 0ab6c6c. Undefined is now imported from jinja2 and checked first, before any truthiness test, so StrictUndefined.__bool__ is never reached:
if isinstance(value, Undefined):
return default
if boolean and not value:
return default
if value is None:
return default
return valueBoth {{ v | default('x') }} and {{ v | default('x', true) }} now return the fallback for a missing variable, while unguarded {{ missing }} still raises under StrictUndefined (fail-fast preserved). Added regression tests for a missing top-level variable in both the two-arg and boolean=True forms (including the positional true shorthand), plus a missing nested dict key.
| Args: | ||
| value: The value to check. | ||
| default: Default value to return if value is None. | ||
| default: Default value to return if value is None or falsey. |
There was a problem hiding this comment.
This reads as though every falsey value uses the fallback. Empty strings, zero, False, and empty collections are preserved unless boolean=True.
| default: Default value to return if value is None or falsey. | |
| default: Value returned for None, or for any falsey value when boolean=True. |
There was a problem hiding this comment.
Reworded in 0ab6c6c to make clear that empty strings, zero, False, and empty collections are preserved unless boolean=True:
default: Value returned for None, Undefined, or for any falsey value
when boolean=True.I used your suggested phrasing and added Undefined since the filter now also returns the fallback for undefined values.
Under StrictUndefined, a missing variable reaches the custom `default`
filter as a StrictUndefined object. The boolean=True path then called
StrictUndefined.__bool__ and raised instead of returning the fallback,
so both `{{ v | default('x') }}` and `{{ v | default('x', true) }}`
raised for undefined variables. This broke templates that use the
documented Jinja calling convention to guard optional values.
Check isinstance(value, Undefined) first so any undefined value returns
the fallback, matching Jinja's built-in default() semantics while keeping
StrictUndefined fail-fast for unguarded expressions. Reword the default
param docstring to describe the actual behavior (fallback for None,
Undefined, or any falsey value when boolean=True). Also correct a stale
comment in workflow.py that claimed default() cannot catch None without
boolean=true.
Add regression tests covering a missing variable in both the two-arg and
boolean=True forms (including the positional true shorthand) and a missing
nested dict key.
Addresses review feedback on microsoft#292. Fixes microsoft#288.
# Conflicts: # src/conductor/executor/template.py
Summary
Fixes #288.
Conductor overrides Jinja2's built-in
defaultfilter with a custom implementation that previously only accepted two parameters. Valid Jinja templates using the standard thirdbooleanargument raisedTypeError: _default_filter() takes from 1 to 2 positional arguments but 3 were given.This PR makes
TemplateRenderer._default_filtersignature-compatible with Jinja's built-indefault(value, default_value="", boolean=False).What changed
src/conductor/executor/template.py—_default_filternow acceptsboolean: bool = False:boolean=False(default): preserves existing Conductor behavior — onlyNonetriggers the default; empty strings,0,False,[],{}are returned as-is.boolean=True: standard Jinja falsey semantics — any falsey value (None,"",0,False,[],{}) returns the default; truthy values pass through unchanged.The parameter name
defaultis kept (not renamed todefault_value) to avoid breaking any existing keyword-argument callers.Tests
tests/test_executor/test_template.py— 14 new tests underTestTemplateRendererDefaultFilter:boolean=Truewith each falsey value (None,"",0,False,[],{}) → defaultboolean=Truewith a truthy value → value returned unchangedNone→ default;"",0,False,[],{}→ returned as-isboolean=Falseregression for the same edge casesVerification
uv run pytest tests/test_executor/test_template.py -v→ 60/60 passedmake check(ruff check + ruff format + ty) → exit 0uv run python -c 'from conductor.executor.template import TemplateRenderer; r = TemplateRenderer(); print(r.render("{{ value | default(\\"x\\", true) }}", {"value": None}))'→ printsxwithoutTypeErrorOut of scope
Per the issue's suggested direction, item 1 ("Return default_value for Jinja
Undefinedvalues") is intentionally not changed here: Conductor runs withStrictUndefined, so undefined variables raiseTemplateErrorbefore reaching the filter — the existing fail-fast behavior is preserved.