Skip to content

Commit ebac419

Browse files
fix: validate instantiated job models with the template's extension context (#362)
create_job instantiated the target models with a bare constructor, so the FEATURE_BUNDLE_1 length validators on Job.name, Environment.name, EmbeddedFileText.name and EmbeddedFileText.filename ran without a parsing context and fell back to the base limits, rejecting values that decode had already accepted under the extension. instantiate_model now seeds a ModelParsingContext from the root template's declared extensions and constructs each target model with model_validate(..., context=...). Resolved values (the job name) are still checked against the extension-aware limit, matching the schema text and the Rust implementation; literal fields keep the result decode already reached. Models whose class binds no parsing-context type are constructed without a context, exactly as before. The two comments in _model.py that described instantiation as running without a context are updated to match; no validator logic changes. Signed-off-by: Yongzhi Wei <276409147+wyongzhi@users.noreply.github.com> Co-authored-by: AlexTranAmz <167144297+AlexTranAmz@users.noreply.github.com>
1 parent a3d4f4e commit ebac419

3 files changed

Lines changed: 403 additions & 17 deletions

File tree

‎src/openjd/model/_internal/_create_job.py‎

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22

33
from contextlib import contextmanager
4-
from typing import Annotated, Any, Union, Dict
4+
from typing import Annotated, Any, Optional, Union, Dict
55

66
from pydantic import ValidationError
77
from pydantic import TypeAdapter
88
from pydantic_core import InitErrorDetails
99

1010
from .._symbol_table import SymbolTable
1111
from .._format_strings import FormatString
12-
from .._types import OpenJDModel
12+
from .._types import ModelParsingContextInterface, OpenJDModel
1313

1414
__all__ = ("instantiate_model", "resolve_whole_field_typed_list")
1515

@@ -147,7 +147,7 @@ def capture_validation_errors(
147147
)
148148

149149

150-
def instantiate_model( # noqa: C901
150+
def instantiate_model(
151151
model: OpenJDModel,
152152
symtab: SymbolTable,
153153
) -> OpenJDModel:
@@ -166,6 +166,33 @@ def instantiate_model( # noqa: C901
166166
167167
Returns:
168168
OpenJDModel: The transformed model.
169+
170+
Target models are constructed with a parsing context seeded from the root
171+
template's declared extensions, so extension-aware limits (e.g. FEATURE_BUNDLE_1
172+
name lengths) apply to resolved values exactly as they did at decode time.
173+
"""
174+
# Models whose class does not bind a parsing-context type (revision-agnostic
175+
# models, e.g. internal test fixtures) are validated without a context, exactly
176+
# as before; every v2023_09 model binds it via its base class.
177+
context_type = getattr(type(model), "model_parsing_context_type", None)
178+
context: Optional[ModelParsingContextInterface]
179+
if context_type is None:
180+
context = None
181+
else:
182+
context = context_type(supported_extensions=getattr(model, "extensions", None) or [])
183+
return _instantiate_model(model, symtab, context=context)
184+
185+
186+
def _instantiate_model( # noqa: C901
187+
model: OpenJDModel,
188+
symtab: SymbolTable,
189+
*,
190+
context: Optional[ModelParsingContextInterface],
191+
) -> OpenJDModel:
192+
"""Recursive worker for :func:`instantiate_model`.
193+
194+
Carries the seeded parsing ``context`` down the depth-first traversal so
195+
each target model is constructed with it.
169196
"""
170197
errors = list[InitErrorDetails]()
171198
instantiated_fields = dict[str, Any]()
@@ -239,16 +266,20 @@ def instantiate_model( # noqa: C901
239266
if field_name in model._job_creation_metadata.reshape_field_to_dict:
240267
key_field = model._job_creation_metadata.reshape_field_to_dict[field_name]
241268
instantiated = _instantiate_list_field_as_dict(
242-
field_value, symtab, needs_resolve, key_field
269+
field_value, symtab, needs_resolve, key_field, context=context
243270
)
244271
else:
245272
instantiated = _instantiate_list_field_as_list(
246-
field_value, symtab, needs_resolve
273+
field_value, symtab, needs_resolve, context=context
247274
)
248275
elif isinstance(field_value, dict):
249-
instantiated = _instantiate_dict_field(field_value, symtab, needs_resolve)
276+
instantiated = _instantiate_dict_field(
277+
field_value, symtab, needs_resolve, context=context
278+
)
250279
else:
251-
instantiated = _instantiate_noncollection_value(field_value, symtab, needs_resolve)
280+
instantiated = _instantiate_noncollection_value(
281+
field_value, symtab, needs_resolve, context=context
282+
)
252283

253284
# Validate as the target field type using cached TypeAdapter
254285
type_adapter = get_type_adapter(target_field_type)
@@ -261,7 +292,7 @@ def instantiate_model( # noqa: C901
261292
instantiated_fields.update(**new_fields)
262293

263294
with capture_validation_errors(output_errors=errors, loc=(), input=field_value):
264-
result = target_model(**instantiated_fields)
295+
result = target_model.model_validate(instantiated_fields, context=context)
265296

266297
if errors:
267298
raise ValidationError.from_exception_data(
@@ -275,6 +306,8 @@ def _instantiate_noncollection_value(
275306
value: Any,
276307
symtab: SymbolTable,
277308
needs_resolve: bool,
309+
*,
310+
context: Optional[ModelParsingContextInterface],
278311
) -> Any:
279312
"""Instantiate a single value that must not be a collection type (list, dict, etc).
280313
@@ -289,7 +322,7 @@ def _instantiate_noncollection_value(
289322
``instantiate_model`` resolves them once up front.
290323
"""
291324
if isinstance(value, OpenJDModel):
292-
return instantiate_model(value, symtab)
325+
return _instantiate_model(value, symtab, context=context)
293326
elif isinstance(value, FormatString) and needs_resolve:
294327
value = value.resolve(symtab=symtab)
295328

@@ -300,6 +333,8 @@ def _instantiate_list_field_as_list( # noqa: C901
300333
value: list[Any],
301334
symtab: SymbolTable,
302335
needs_resolve: bool,
336+
*,
337+
context: Optional[ModelParsingContextInterface],
303338
) -> list[Any]:
304339
"""As _instantiate_noncollection_value, but where the value is a list.
305340
@@ -319,6 +354,7 @@ def _instantiate_list_field_as_list( # noqa: C901
319354
item,
320355
symtab,
321356
needs_resolve,
357+
context=context,
322358
)
323359
)
324360

@@ -331,7 +367,12 @@ def _instantiate_list_field_as_list( # noqa: C901
331367

332368

333369
def _instantiate_list_field_as_dict( # noqa: C901
334-
value: list[Any], symtab: SymbolTable, needs_resolve: bool, key_field: str
370+
value: list[Any],
371+
symtab: SymbolTable,
372+
needs_resolve: bool,
373+
key_field: str,
374+
*,
375+
context: Optional[ModelParsingContextInterface],
335376
) -> dict[str, Any]:
336377
"""As _instantiate_noncollection_value, but where the value is a list.
337378
@@ -351,6 +392,7 @@ def _instantiate_list_field_as_dict( # noqa: C901
351392
item,
352393
symtab,
353394
needs_resolve,
395+
context=context,
354396
)
355397

356398
if errors:
@@ -365,6 +407,8 @@ def _instantiate_dict_field(
365407
value: dict[str, Any],
366408
symtab: SymbolTable,
367409
needs_resolve: bool,
410+
*,
411+
context: Optional[ModelParsingContextInterface],
368412
) -> dict[str, Any]:
369413
"""As _instantiate_noncollection_value, but where the value is a dict.
370414
@@ -382,6 +426,7 @@ def _instantiate_dict_field(
382426
item,
383427
symtab,
384428
needs_resolve,
429+
context=context,
385430
)
386431

387432
if errors:

‎src/openjd/model/v2023_09/_model.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -734,11 +734,13 @@ def _requires_oneof(cls, values: dict[str, Any], info: ValidationInfo) -> dict[s
734734
any_wrap = any(v is not None for v in wrap_values.values())
735735

736736
if any_wrap:
737-
# Extension gating is a template-decode concern and only applies
738-
# when a parsing context is present. During job instantiation
739-
# (create_job re-validates the model without a ModelParsingContext)
740-
# the template has already been validated at decode time, so the
741-
# extension-requirement checks are skipped then -- mirroring the
737+
# Extension gating only applies when a parsing context is present.
738+
# At decode time the context carries the supported set; during job
739+
# instantiation create_job re-validates the model with a context
740+
# seeded from the template's declared extensions (see
741+
# _internal/_create_job.py), so this check reproduces its decode-time
742+
# result. A missing context only occurs for model classes that bind no
743+
# parsing-context type, and the check is skipped then -- mirroring the
742744
# `if context` guard the other extension gates in this module use.
743745
if context is not None:
744746
if "WRAP_ACTIONS" not in extensions:
@@ -898,7 +900,8 @@ def _validate_end_of_line(
898900
if v is None:
899901
return v
900902
context = cast(Optional[ModelParsingContext], info.context)
901-
# Skip extension check if no context (e.g., during job creation from validated template)
903+
# Skip only when there is no parsing context (model classes that bind no
904+
# context type); create_job seeds one from the template's declared extensions.
902905
if context and "FEATURE_BUNDLE_1" not in context.extensions:
903906
raise ValueError("The endOfLine property requires the FEATURE_BUNDLE_1 extension.")
904907
return v

0 commit comments

Comments
 (0)