From f6095d56b24618b0635c32933ebbdb590503f783 Mon Sep 17 00:00:00 2001 From: David Leong Date: Fri, 14 Aug 2026 18:39:22 +0000 Subject: [PATCH] fix: do not cap expansion at the list-form limit Signed-off-by: David Leong --- src/openjd/model/v2023_09/_model.py | 41 ++++-------- .../model_v0/v2023_09/test_parameter_space.py | 64 +++++++++++++++++++ 2 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/openjd/model/v2023_09/_model.py b/src/openjd/model/v2023_09/_model.py index 2e0432b2..8d7ae71a 100644 --- a/src/openjd/model/v2023_09/_model.py +++ b/src/openjd/model/v2023_09/_model.py @@ -980,9 +980,14 @@ def validate_let_field(value: Any, info: ValidationInfo, *, simple_action: bool return value -# §3.4: the maximum number of values a task parameter's range may take on. -# Not raised by FEATURE_BUNDLE_1 in 2023-09 (matches openjd-rs's -# EffectiveLimits.max_task_param_range_len). +# §3.4: the maximum number of elements in a task parameter's *list*-form range +# — `` (§3.4.1.1), `` (§3.4.1.2) and +# `` (§3.4.1.3). Not raised by FEATURE_BUNDLE_1 in 2023-09. +# +# Do not apply this to an `` expansion. §3.4.1.1.1 constrains that +# form only by "no two ranges may overlap" and states its purpose is to express +# frame ranges succinctly, so capping the expansion rejects the form's primary +# use case and pre-empts the host service's own task-count limits. _MAX_TASK_PARAM_RANGE_LEN = 1024 @@ -1289,26 +1294,6 @@ class RangeExpressionTaskParameterDefinition(OpenJDModel_v2023_09): # has a value when type is CHUNK[INT], which is only possible from the TASK_CHUNKING extension chunks: Optional[TaskChunksDefinition] = None - @field_validator("range") - @classmethod - def _validate_range_len(cls, value: Any) -> Any: - # §3.4: a range expression that arrives via format-string resolution - # (e.g. `range: "{{RawParam.Frames}}"` with a RANGE_EXPR parameter) is - # only parsed at instantiation, so the expansion cap must be enforced - # here too — matching openjd-rs's resolve-time checks in create_job. - if isinstance(value, IntRangeExpr): - _check_range_expr_len(value) - return value - - -def _check_range_expr_len(parsed_range: IntRangeExpr) -> None: - """§3.4: a range expression may expand to at most 1024 values.""" - if len(parsed_range) > _MAX_TASK_PARAM_RANGE_LEN: - raise ValueError( - f"range expression expands to {len(parsed_range)} elements " - f"(max {_MAX_TASK_PARAM_RANGE_LEN})." - ) - def _range_task_param_target(model: Any, typed_values: dict) -> Type[OpenJDModel]: """``create_as`` target-model selector shared by the INT and CHUNK[INT] @@ -1412,9 +1397,9 @@ def _native_element_type_name(elem: Any) -> str: def _validate_int_range_elements(value: Any) -> Any: """Shared ``range`` post-validator for the INT and CHUNK[INT] task-parameter definitions: a literal range-expression string must parse - and may expand to at most 1024 values (§3.4); a list-form range is - length-capped. Ranges containing format expressions defer to the - RangeExpressionTaskParameterDefinition model once they are resolved. + against the ```` grammar; a list-form range is length-capped + (§3.4). The expansion of a range expression is deliberately not capped — + see ``_MAX_TASK_PARAM_RANGE_LEN``. """ if isinstance(value, FormatString): # If there are no format expressions, we can validate the range expression. @@ -1422,11 +1407,9 @@ def _validate_int_range_elements(value: Any) -> Any: # they've all been evaluated if len(value.expressions) == 0: try: - parsed_range = IntRangeExpr.from_str(value) + IntRangeExpr.from_str(value) except Exception as e: raise ValueError(str(e)) - # §3.4: the range may take on at most 1024 values. - _check_range_expr_len(parsed_range) else: validate_task_param_range_list_len(value) return value diff --git a/test/openjd/model_v0/v2023_09/test_parameter_space.py b/test/openjd/model_v0/v2023_09/test_parameter_space.py index cf07fa2b..23f0af9e 100644 --- a/test/openjd/model_v0/v2023_09/test_parameter_space.py +++ b/test/openjd/model_v0/v2023_09/test_parameter_space.py @@ -10,6 +10,8 @@ FloatTaskParameterDefinition, IntTaskParameterDefinition, PathTaskParameterDefinition, + RangeExpressionTaskParameterDefinition, + RangeListTaskParameterDefinition, StepParameterSpaceDefinition, StringTaskParameterDefinition, ) @@ -329,6 +331,10 @@ class TestRangeExpressionTaskParameterDefinition: }, id="format string with multiple", ), + pytest.param( + {"name": "foo", "type": "INT", "range": "1-5000"}, + id="expansion past the list-form cap", + ), ), ) def test_parse_success(self, data: dict[str, str]) -> None: @@ -375,6 +381,64 @@ def test_parse_fails(self, data: dict[str, Any]) -> None: assert len(excinfo.value.errors()) > 0 +class TestTaskParameterRangeLength: + """§3.4 caps the number of elements in the *list* forms of a task parameter's + range. §3.4.1.1.1 `` carries no element cap, so an expression's + expansion must not be capped — the form exists to express frame ranges, which + routinely run to thousands of values. + """ + + @pytest.mark.parametrize( + "range_expr,expected_len", + ( + pytest.param("1-1024", 1024, id="at the list-form cap"), + pytest.param("1-1025", 1025, id="one past the list-form cap"), + pytest.param("1-5000", 5000, id="ordinary frame range"), + pytest.param("1-100000:2", 50000, id="large range with a step"), + ), + ) + def test_range_expression_expansion_is_not_capped( + self, range_expr: str, expected_len: int + ) -> None: + # WHEN the template-layer definition parses a literal range expression + _parse_model( + model=IntTaskParameterDefinition, + obj={"name": "foo", "type": "INT", "range": range_expr}, + ) + + # AND the instantiation target parses the same expression + instantiated = _parse_model( + model=RangeExpressionTaskParameterDefinition, + obj={"type": "INT", "range": range_expr}, + ) + + # THEN neither rejects it, and the range expands in full + assert len(instantiated.range) == expected_len + + @pytest.mark.parametrize( + "model,obj", + ( + pytest.param( + IntTaskParameterDefinition, + {"name": "foo", "type": "INT", "range": [1] * 1025}, + id="template layer", + ), + pytest.param( + RangeListTaskParameterDefinition, + {"type": "INT", "range": [1] * 1025}, + id="instantiation layer", + ), + ), + ) + def test_list_form_range_is_still_capped(self, model: Any, obj: dict[str, Any]) -> None: + # WHEN a list-form range one element past the §3.4 cap is parsed + with pytest.raises(ValidationError) as excinfo: + _parse_model(model=model, obj=obj) + + # THEN it is rejected + assert len(excinfo.value.errors()) > 0 + + class TestStepParameterSpaceDefinition: @pytest.mark.parametrize( "data",