Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@


_ENV_REFERENCE_RE = re.compile(r"\bEnv\.([A-Za-z_][A-Za-z0-9_]*)")
_INPUT_REFERENCE_RE = re.compile(r"(?<![\w.])(?:inputs|Workflow\.Inputs)\.([A-Za-z_][A-Za-z0-9_]*)")


def _bind_missing_input_references(formula: str, symbols: dict[str, Any]) -> dict[str, Any]:
"""Expose inputs the formula references but the caller did not supply as blank.

PowerFx rejects ``inputs.name`` at compile time when ``name`` is not a field of the
inputs record, which turns the whole expression into ``None`` and defeats defaults
such as ``If(IsBlank(inputs.name), "World", inputs.name)``. Binding the missing
fields to blank only for this evaluation makes them behave like an explicit ``None``
input without changing the workflow state.
"""
inputs = symbols.get("inputs")
if not isinstance(inputs, Mapping):
return symbols
input_values = cast(Mapping[str, Any], inputs)
missing = {name for name in _INPUT_REFERENCE_RE.findall(formula) if name not in input_values}
if not missing:
return symbols
bound_inputs: dict[str, Any] = {**input_values, **dict.fromkeys(sorted(missing))}
return {**symbols, "Workflow": {**symbols["Workflow"], "Inputs": bound_inputs}, "inputs": bound_inputs}


@dataclass(frozen=True)
Expand Down Expand Up @@ -605,7 +626,7 @@ def eval(self, expression: str) -> Any:
f"Install dotnet and the powerfx package for full PowerFx support."
)

symbols = self._to_powerfx_symbols()
symbols = _bind_missing_input_references(formula, self._to_powerfx_symbols())
# Use setlocale(category) query form so we can restore the exact prior value.
# getlocale() returns a normalized tuple and is not always a lossless
# round-trip for setlocale across platforms/locales.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,23 @@ async def test_if_with_isblank(self, mock_state):
result = state.eval('=If(IsBlank(Workflow.Inputs.name), "World", Workflow.Inputs.name)')
assert result == "Alice"

@pytest.mark.parametrize(
"expression",
[
'=If(IsBlank(inputs.name), "World", inputs.name)',
'=If(IsBlank(Workflow.Inputs.name), "World", Workflow.Inputs.name)',
],
)
async def test_if_with_isblank_when_input_is_missing(self, mock_state, expression):
"""Test that an input the caller did not supply is blank, so IsBlank defaults apply."""
state = DeclarativeWorkflowState(mock_state)
state.initialize({"other": 1})

assert state.eval(expression) == "World"
assert state.eval("=IsBlank(inputs.name)") is True
# The missing input is only bound for evaluation, not written to state.
assert "name" not in state.get_state_data()["Inputs"]

async def test_not_function(self, mock_state):
"""Test Not function."""
state = DeclarativeWorkflowState(mock_state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ async def main() -> None:
# Run with a custom name
print("\nRunning with custom name 'Alice':")
result = await workflow.run({"name": "Alice"})
for output in result.get_outputs():
print(f" Output: {output}")
print("\n" + "-" * 40)
print("Workflow completed!")

Expand Down
Loading