Skip to content

Commit f6095d5

Browse files
committed
fix: do not cap <IntRangeExpr> expansion at the list-form limit
Signed-off-by: David Leong <leongdl@amazon.com>
1 parent 25575ae commit f6095d5

2 files changed

Lines changed: 76 additions & 29 deletions

File tree

src/openjd/model/v2023_09/_model.py

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -980,9 +980,14 @@ def validate_let_field(value: Any, info: ValidationInfo, *, simple_action: bool
980980
return value
981981

982982

983-
# §3.4: the maximum number of values a task parameter's range may take on.
984-
# Not raised by FEATURE_BUNDLE_1 in 2023-09 (matches openjd-rs's
985-
# EffectiveLimits.max_task_param_range_len).
983+
# §3.4: the maximum number of elements in a task parameter's *list*-form range
984+
# — `<IntRangeList>` (§3.4.1.1), `<FloatRangeList>` (§3.4.1.2) and
985+
# `<StringRangeList>` (§3.4.1.3). Not raised by FEATURE_BUNDLE_1 in 2023-09.
986+
#
987+
# Do not apply this to an `<IntRangeExpr>` expansion. §3.4.1.1.1 constrains that
988+
# form only by "no two ranges may overlap" and states its purpose is to express
989+
# frame ranges succinctly, so capping the expansion rejects the form's primary
990+
# use case and pre-empts the host service's own task-count limits.
986991
_MAX_TASK_PARAM_RANGE_LEN = 1024
987992

988993

@@ -1289,26 +1294,6 @@ class RangeExpressionTaskParameterDefinition(OpenJDModel_v2023_09):
12891294
# has a value when type is CHUNK[INT], which is only possible from the TASK_CHUNKING extension
12901295
chunks: Optional[TaskChunksDefinition] = None
12911296

1292-
@field_validator("range")
1293-
@classmethod
1294-
def _validate_range_len(cls, value: Any) -> Any:
1295-
# §3.4: a range expression that arrives via format-string resolution
1296-
# (e.g. `range: "{{RawParam.Frames}}"` with a RANGE_EXPR parameter) is
1297-
# only parsed at instantiation, so the expansion cap must be enforced
1298-
# here too — matching openjd-rs's resolve-time checks in create_job.
1299-
if isinstance(value, IntRangeExpr):
1300-
_check_range_expr_len(value)
1301-
return value
1302-
1303-
1304-
def _check_range_expr_len(parsed_range: IntRangeExpr) -> None:
1305-
"""§3.4: a range expression may expand to at most 1024 values."""
1306-
if len(parsed_range) > _MAX_TASK_PARAM_RANGE_LEN:
1307-
raise ValueError(
1308-
f"range expression expands to {len(parsed_range)} elements "
1309-
f"(max {_MAX_TASK_PARAM_RANGE_LEN})."
1310-
)
1311-
13121297

13131298
def _range_task_param_target(model: Any, typed_values: dict) -> Type[OpenJDModel]:
13141299
"""``create_as`` target-model selector shared by the INT and CHUNK[INT]
@@ -1412,21 +1397,19 @@ def _native_element_type_name(elem: Any) -> str:
14121397
def _validate_int_range_elements(value: Any) -> Any:
14131398
"""Shared ``range`` post-validator for the INT and CHUNK[INT]
14141399
task-parameter definitions: a literal range-expression string must parse
1415-
and may expand to at most 1024 values (§3.4); a list-form range is
1416-
length-capped. Ranges containing format expressions defer to the
1417-
RangeExpressionTaskParameterDefinition model once they are resolved.
1400+
against the ``<IntRangeExpr>`` grammar; a list-form range is length-capped
1401+
(§3.4). The expansion of a range expression is deliberately not capped —
1402+
see ``_MAX_TASK_PARAM_RANGE_LEN``.
14181403
"""
14191404
if isinstance(value, FormatString):
14201405
# If there are no format expressions, we can validate the range expression.
14211406
# otherwise we defer to the RangeExressionTaskParameter model when
14221407
# they've all been evaluated
14231408
if len(value.expressions) == 0:
14241409
try:
1425-
parsed_range = IntRangeExpr.from_str(value)
1410+
IntRangeExpr.from_str(value)
14261411
except Exception as e:
14271412
raise ValueError(str(e))
1428-
# §3.4: the range may take on at most 1024 values.
1429-
_check_range_expr_len(parsed_range)
14301413
else:
14311414
validate_task_param_range_list_len(value)
14321415
return value

test/openjd/model_v0/v2023_09/test_parameter_space.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
FloatTaskParameterDefinition,
1111
IntTaskParameterDefinition,
1212
PathTaskParameterDefinition,
13+
RangeExpressionTaskParameterDefinition,
14+
RangeListTaskParameterDefinition,
1315
StepParameterSpaceDefinition,
1416
StringTaskParameterDefinition,
1517
)
@@ -329,6 +331,10 @@ class TestRangeExpressionTaskParameterDefinition:
329331
},
330332
id="format string with multiple",
331333
),
334+
pytest.param(
335+
{"name": "foo", "type": "INT", "range": "1-5000"},
336+
id="expansion past the list-form cap",
337+
),
332338
),
333339
)
334340
def test_parse_success(self, data: dict[str, str]) -> None:
@@ -375,6 +381,64 @@ def test_parse_fails(self, data: dict[str, Any]) -> None:
375381
assert len(excinfo.value.errors()) > 0
376382

377383

384+
class TestTaskParameterRangeLength:
385+
"""§3.4 caps the number of elements in the *list* forms of a task parameter's
386+
range. §3.4.1.1.1 `<IntRangeExpr>` carries no element cap, so an expression's
387+
expansion must not be capped — the form exists to express frame ranges, which
388+
routinely run to thousands of values.
389+
"""
390+
391+
@pytest.mark.parametrize(
392+
"range_expr,expected_len",
393+
(
394+
pytest.param("1-1024", 1024, id="at the list-form cap"),
395+
pytest.param("1-1025", 1025, id="one past the list-form cap"),
396+
pytest.param("1-5000", 5000, id="ordinary frame range"),
397+
pytest.param("1-100000:2", 50000, id="large range with a step"),
398+
),
399+
)
400+
def test_range_expression_expansion_is_not_capped(
401+
self, range_expr: str, expected_len: int
402+
) -> None:
403+
# WHEN the template-layer definition parses a literal range expression
404+
_parse_model(
405+
model=IntTaskParameterDefinition,
406+
obj={"name": "foo", "type": "INT", "range": range_expr},
407+
)
408+
409+
# AND the instantiation target parses the same expression
410+
instantiated = _parse_model(
411+
model=RangeExpressionTaskParameterDefinition,
412+
obj={"type": "INT", "range": range_expr},
413+
)
414+
415+
# THEN neither rejects it, and the range expands in full
416+
assert len(instantiated.range) == expected_len
417+
418+
@pytest.mark.parametrize(
419+
"model,obj",
420+
(
421+
pytest.param(
422+
IntTaskParameterDefinition,
423+
{"name": "foo", "type": "INT", "range": [1] * 1025},
424+
id="template layer",
425+
),
426+
pytest.param(
427+
RangeListTaskParameterDefinition,
428+
{"type": "INT", "range": [1] * 1025},
429+
id="instantiation layer",
430+
),
431+
),
432+
)
433+
def test_list_form_range_is_still_capped(self, model: Any, obj: dict[str, Any]) -> None:
434+
# WHEN a list-form range one element past the §3.4 cap is parsed
435+
with pytest.raises(ValidationError) as excinfo:
436+
_parse_model(model=model, obj=obj)
437+
438+
# THEN it is rejected
439+
assert len(excinfo.value.errors()) > 0
440+
441+
378442
class TestStepParameterSpaceDefinition:
379443
@pytest.mark.parametrize(
380444
"data",

0 commit comments

Comments
 (0)