Skip to content

Commit b255be6

Browse files
committed
feat: Add WrappedAction.Cancelation.* wrap-hook template variables
Register WrappedAction.Cancelation.Mode and WrappedAction.Cancelation.NotifyPeriodInSeconds in the environment script's injected-symbol list so format strings referencing them pass validation, per the RFC 0008 follow-up (OpenJobDescription/openjd-specifications#148): - Mode is string? — "TERMINATE", "NOTIFY_THEN_TERMINATE", or null when the wrapped action defines no <Cancelation>, following the EXPR semantics for optional data. The null case is distinct from an explicit TERMINATE so wrap scripts can tell "author declared TERMINATE" apart from "author declared nothing". - NotifyPeriodInSeconds is int? — the effective grace period when the mode is NOTIFY_THEN_TERMINATE (with the Template Schemas 5.3.2 defaults applied: 120 for a task's onRun, 30 otherwise), and null when a notify period does not apply. Also make the legacy (non-EXPR) format-string node render a None value as the empty string, matching the EXPR engine's null rendering (RFC 0005). This is required for both nullable variables, whose None values must not render as the Python repr "None". Runtime seeding of the two variables lands in openjd-sessions-for-python; this is the model-side validation and rendering support. Mirrors the Rust implementation in OpenJobDescription/openjd-rs#261. Verified by unit tests and the wrap-cancelation-* conformance fixtures — the full WRAP_ACTIONS suite (53 tests) passes against the Python CLI. Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
1 parent 9d8f7b8 commit b255be6

4 files changed

Lines changed: 70 additions & 1 deletion

File tree

src/openjd/model/_format_strings/_nodes.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,17 @@ def evaluate_to_str(self, *, symtab: SymbolTable, path_format: Any = None) -> st
6666
EXPR-backed nodes override this to use the engine's own spec-defined
6767
coercion (RFC 0005), so e.g. ``true``/``false``/``null`` and lists
6868
render per the specification rather than as Python reprs.
69+
70+
A ``None`` value interpolates as the empty string, matching the EXPR
71+
engine's null rendering (RFC 0005) — relevant for nullable injected
72+
symbols such as ``WrappedAction.Cancelation.NotifyPeriodInSeconds``
73+
(RFC 0008 follow-up), which is ``None`` when no notify period
74+
applies.
6975
"""
70-
return str(self.evaluate(symtab=symtab, path_format=path_format))
76+
value = self.evaluate(symtab=symtab, path_format=path_format)
77+
if value is None:
78+
return ""
79+
return str(value)
7180

7281
@abstractmethod
7382
def __repr__(self) -> str: # pragma: no cover

src/openjd/model/v2023_09/_model.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,15 @@ class EnvironmentScript(OpenJDModel_v2023_09):
903903
"|WrappedAction.Args",
904904
"|WrappedAction.Environment",
905905
"|WrappedAction.Timeout",
906+
# RFC 0008 follow-up (openjd-specifications#148): the wrapped
907+
# action's cancelation config. Mode is string? — "TERMINATE",
908+
# "NOTIFY_THEN_TERMINATE", or null when the wrapped action
909+
# defines no <Cancelation>. NotifyPeriodInSeconds is int? — the effective
910+
# grace period for NOTIFY_THEN_TERMINATE (with the schema
911+
# defaults applied: 120 for a task's onRun, 30 otherwise), and
912+
# null when a notify period does not apply.
913+
"|WrappedAction.Cancelation.Mode",
914+
"|WrappedAction.Cancelation.NotifyPeriodInSeconds",
906915
"|WrappedEnv.Name",
907916
"|WrappedStep.Name",
908917
},

test/openjd/model_v0/format_strings/test_format_string.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,27 @@ def test_multiple_expressions(
108108
# THEN
109109
assert format_string.resolve(symtab=symtab) == expected
110110

111+
@pytest.mark.parametrize(
112+
"input, expected",
113+
[
114+
pytest.param("MODE=<{{Test.val}}>", "MODE=<>", id="none-only"),
115+
pytest.param("{{Test.val}}", "", id="whole-string-none"),
116+
],
117+
)
118+
def test_none_value_renders_as_empty(self, input: str, expected: str) -> None:
119+
# A None value interpolates as the empty string, matching the EXPR
120+
# engine's null rendering (RFC 0005). Relevant for the nullable
121+
# WrappedAction.Cancelation.* injected symbols (RFC 0008 follow-up).
122+
# GIVEN
123+
symtab = SymbolTable()
124+
125+
# WHEN
126+
format_string = FormatString(input, context=ModelParsingContext_v2023_09())
127+
symtab["Test.val"] = None
128+
129+
# THEN
130+
assert format_string.resolve(symtab=symtab) == expected
131+
111132
def test_without_entry_in_table(self):
112133
# GIVEN
113134
input = " {{ Test.val }}-{{ Test.end}} "

test/openjd/model_v0/format_strings/test_node.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,33 @@ def test_repr(self):
3939

4040
# THEN
4141
assert str(node) == "FullName(Test.Name)"
42+
43+
def test_evaluate_to_str_renders_none_as_empty(self):
44+
# A None value interpolates as the empty string, matching the EXPR
45+
# engine's null rendering (RFC 0005). Relevant for nullable injected
46+
# symbols such as WrappedAction.Cancelation.Mode (string?) and
47+
# WrappedAction.Cancelation.NotifyPeriodInSeconds (int?), which are
48+
# None when the wrapped action defines no <Cancelation>
49+
# (RFC 0008 follow-up).
50+
# GIVEN
51+
symtab = SymbolTable()
52+
symtab["Test.Name"] = None
53+
node = FullNameNode("Test.Name")
54+
55+
# WHEN
56+
result = node.evaluate_to_str(symtab=symtab)
57+
58+
# THEN
59+
assert result == ""
60+
61+
def test_evaluate_to_str_coerces_value_with_str(self):
62+
# GIVEN
63+
symtab = SymbolTable()
64+
symtab["Test.Name"] = 45
65+
node = FullNameNode("Test.Name")
66+
67+
# WHEN
68+
result = node.evaluate_to_str(symtab=symtab)
69+
70+
# THEN
71+
assert result == "45"

0 commit comments

Comments
 (0)