Skip to content

Commit d3411b7

Browse files
committed
[components] Update resolution deferral logic
1 parent 800aeb3 commit d3411b7

File tree

9 files changed

+59
-66
lines changed

9 files changed

+59
-66
lines changed

examples/docs_beta_snippets/docs_beta_snippets/guides/components/shell-script-component/defining-resolvable-field.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class ShellScriptSchema(ComponentSchemaBaseModel):
1414
script_runner: Annotated[
1515
str,
1616
ResolvableFieldInfo(
17-
output_type=ScriptRunner, additional_scope={"get_script_runner"}
17+
output_type=ScriptRunner, required_scope={"get_script_runner"}
1818
),
1919
]
2020
# highlight-end

python_modules/libraries/dagster-components/dagster_components/core/component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def get_registered_component_types_in_module(module: ModuleType) -> Iterable[typ
211211
yield component
212212

213213

214-
T = TypeVar("T")
214+
T = TypeVar("T", bound=BaseModel)
215215

216216

217217
@dataclass

python_modules/libraries/dagster-components/dagster_components/core/schema/base.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@
33

44
from pydantic import BaseModel, ConfigDict, TypeAdapter
55

6-
from dagster_components.core.schema.metadata import (
7-
JSON_SCHEMA_EXTRA_DEFER_RENDERING_KEY,
8-
get_resolution_metadata,
9-
)
6+
from dagster_components.core.schema.metadata import get_resolution_metadata
107
from dagster_components.core.schema.resolver import TemplatedValueResolver
118

129

1310
class ComponentSchemaBaseModel(BaseModel):
1411
"""Base class for models that are part of a component schema."""
1512

16-
model_config = ConfigDict(
17-
json_schema_extra={JSON_SCHEMA_EXTRA_DEFER_RENDERING_KEY: True}, extra="forbid"
18-
)
13+
model_config = ConfigDict(extra="forbid")
1914

2015
def resolve_properties(self, value_resolver: TemplatedValueResolver) -> Mapping[str, Any]:
2116
"""Returns a dictionary of resolved properties for this class."""

python_modules/libraries/dagster-components/dagster_components/core/schema/metadata.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
REF_BASE = "#/$defs/"
99
REF_TEMPLATE = f"{REF_BASE}{{model}}"
1010
JSON_SCHEMA_EXTRA_DEFER_RENDERING_KEY = "dagster_defer_rendering"
11-
JSON_SCHEMA_EXTRA_AVAILABLE_SCOPE_KEY = "dagster_available_scope"
11+
JSON_SCHEMA_EXTRA_REQUIRED_SCOPE_KEY = "dagster_required_scope"
1212

1313

1414
@dataclass
@@ -34,7 +34,7 @@ def __init__(
3434
*,
3535
output_type: Optional[type] = None,
3636
post_process_fn: Optional[Callable[[Any], Any]] = None,
37-
additional_scope: Optional[Set[str]] = None,
37+
required_scope: Optional[Set[str]] = None,
3838
):
3939
self.resolution_metadata = (
4040
ResolutionMetadata(output_type=output_type, post_process=post_process_fn)
@@ -43,8 +43,11 @@ def __init__(
4343
)
4444
super().__init__(
4545
json_schema_extra={
46-
JSON_SCHEMA_EXTRA_AVAILABLE_SCOPE_KEY: list(additional_scope or []),
47-
JSON_SCHEMA_EXTRA_DEFER_RENDERING_KEY: True,
46+
JSON_SCHEMA_EXTRA_REQUIRED_SCOPE_KEY: list(required_scope or []),
47+
# defer resolution if the output type will change
48+
**(
49+
{JSON_SCHEMA_EXTRA_DEFER_RENDERING_KEY: True} if output_type is not None else {}
50+
),
4851
},
4952
)
5053

@@ -98,33 +101,34 @@ def _subschemas_on_path(
98101
yield from _subschemas_on_path(rest, json_schema, inner)
99102

100103

101-
def _should_defer_render(subschema: Mapping[str, Any]) -> bool:
104+
def _get_should_defer_resolve(subschema: Mapping[str, Any]) -> bool:
102105
raw = check.opt_inst(subschema.get(JSON_SCHEMA_EXTRA_DEFER_RENDERING_KEY), bool)
103106
return raw or False
104107

105108

106-
def _get_available_scope(subschema: Mapping[str, Any]) -> Set[str]:
107-
raw = check.opt_inst(subschema.get(JSON_SCHEMA_EXTRA_AVAILABLE_SCOPE_KEY), list)
109+
def _get_additional_required_scope(subschema: Mapping[str, Any]) -> Set[str]:
110+
raw = check.opt_inst(subschema.get(JSON_SCHEMA_EXTRA_REQUIRED_SCOPE_KEY), list)
108111
return set(raw) if raw else set()
109112

110113

111114
def allow_resolve(
112115
valpath: Sequence[Union[str, int]], json_schema: Mapping[str, Any], subschema: Mapping[str, Any]
113116
) -> bool:
114-
"""Given a valpath and the json schema of a given target type, determines if there is a rendering scope
115-
required to render the value at the given path.
117+
"""Given a valpath and the json schema of a given target type, determines if this value can be
118+
resolved eagerly. This can only happen if the output type of the resolved value is unchanged,
119+
and there is no additional scope required for resolution.
116120
"""
117121
for subschema in _subschemas_on_path(valpath, json_schema, subschema):
118-
if _should_defer_render(subschema):
122+
if _get_should_defer_resolve(subschema) or _get_additional_required_scope(subschema):
119123
return False
120124
return True
121125

122126

123-
def get_available_scope(
124-
valpath: Sequence[Union[str, int]], json_schema: Mapping[str, Any], subschema: Mapping[str, Any]
127+
def get_required_scope(
128+
valpath: Sequence[Union[str, int]], json_schema: Mapping[str, Any]
125129
) -> Set[str]:
126130
"""Given a valpath and the json schema of a given target type, determines the available rendering scope."""
127-
available_scope = set()
128-
for subschema in _subschemas_on_path(valpath, json_schema, subschema):
129-
available_scope |= _get_available_scope(subschema)
130-
return available_scope
131+
required_scope = set()
132+
for subschema in _subschemas_on_path(valpath, json_schema, json_schema):
133+
required_scope |= _get_additional_required_scope(subschema)
134+
return required_scope

python_modules/libraries/dagster-components/dagster_components/core/schema/resolver.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ def automation_condition_scope() -> Mapping[str, Any]:
2626
}
2727

2828

29-
ShouldResolveFn = Callable[[Sequence[Union[str, int]]], bool]
30-
31-
3229
@record
3330
class TemplatedValueResolver:
3431
scope: Mapping[str, Any]
@@ -50,23 +47,23 @@ def _resolve_obj(
5047
self,
5148
obj: Any,
5249
valpath: Optional[Sequence[Union[str, int]]],
53-
should_render: Callable[[Sequence[Union[str, int]]], bool],
50+
should_resolve: Callable[[Sequence[Union[str, int]]], bool],
5451
) -> Any:
55-
"""Recursively resolves templated values in a nested object, based on the provided should_render function."""
56-
if valpath is not None and not should_render(valpath):
52+
"""Recursively resolves templated values in a nested object, based on the provided should_resolve function."""
53+
if valpath is not None and not should_resolve(valpath):
5754
return obj
5855
elif isinstance(obj, dict):
59-
# render all values in the dict
56+
# resolve all values in the dict
6057
return {
6158
k: self._resolve_obj(
62-
v, [*valpath, k] if valpath is not None else None, should_render
59+
v, [*valpath, k] if valpath is not None else None, should_resolve
6360
)
6461
for k, v in obj.items()
6562
}
6663
elif isinstance(obj, list):
67-
# render all values in the list
64+
# resolve all values in the list
6865
return [
69-
self._resolve_obj(v, [*valpath, i] if valpath is not None else None, should_render)
66+
self._resolve_obj(v, [*valpath, i] if valpath is not None else None, should_resolve)
7067
for i, v in enumerate(obj)
7168
]
7269
else:
@@ -76,15 +73,12 @@ def resolve_obj(self, val: Any) -> Any:
7673
"""Recursively resolves templated values in a nested object."""
7774
return self._resolve_obj(val, None, lambda _: True)
7875

79-
def resolve_params(self, val: T, target_type: type) -> T:
80-
"""Given a raw params value, preprocesses it by rendering any templated values that are not marked as deferred in the target_type's json schema."""
81-
json_schema = (
82-
target_type.model_json_schema() if issubclass(target_type, BaseModel) else None
76+
def resolve_params(self, val: T, target_type: type[BaseModel]) -> T:
77+
"""Given a raw params value, preprocesses it by resolving any templated values that are not marked
78+
as deferred in the target_type's json schema.
79+
"""
80+
json_schema = target_type.model_json_schema()
81+
should_resolve = functools.partial(
82+
allow_resolve, json_schema=json_schema, subschema=json_schema
8383
)
84-
if json_schema is None:
85-
should_render = lambda _: True
86-
else:
87-
should_render = functools.partial(
88-
allow_resolve, json_schema=json_schema, subschema=json_schema
89-
)
90-
return self._resolve_obj(val, [], should_render=should_render)
84+
return self._resolve_obj(val, [], should_resolve=should_resolve)

python_modules/libraries/dagster-components/dagster_components/lib/dbt_project/component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class DbtProjectParams(BaseModel):
3030
dbt: DbtCliResource
3131
op: Optional[OpSpecBaseModel] = None
3232
asset_attributes: Annotated[
33-
Optional[AssetAttributesModel], ResolvableFieldInfo(additional_scope={"node"})
33+
Optional[AssetAttributesModel], ResolvableFieldInfo(required_scope={"node"})
3434
] = None
3535
transforms: Optional[Sequence[AssetSpecTransformModel]] = None
3636

python_modules/libraries/dagster-components/dagster_components/lib/sling_replication_collection/component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class SlingReplicationParams(BaseModel):
2727
path: str
2828
op: Optional[OpSpecBaseModel] = None
2929
asset_attributes: Annotated[
30-
Optional[AssetAttributesModel], ResolvableFieldInfo(additional_scope={"stream_definition"})
30+
Optional[AssetAttributesModel], ResolvableFieldInfo(required_scope={"stream_definition"})
3131
] = None
3232

3333

python_modules/libraries/dagster-components/dagster_components/lib/test/complex_schema_asset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ComplexAssetParams(BaseModel):
2121
value: str
2222
op: Optional[OpSpecBaseModel] = None
2323
asset_attributes: Annotated[
24-
Optional[AssetAttributesModel], ResolvableFieldInfo(additional_scope={"node"})
24+
Optional[AssetAttributesModel], ResolvableFieldInfo(required_scope={"node"})
2525
] = None
2626
asset_transforms: Optional[Sequence[AssetSpecTransformModel]] = None
2727

python_modules/libraries/dagster-components/dagster_components_tests/rendering_tests/test_schema_resolution.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33

44
import pytest
55
from dagster_components import ComponentSchemaBaseModel, ResolvableFieldInfo, TemplatedValueResolver
6-
from dagster_components.core.schema.metadata import allow_resolve, get_available_scope
6+
from dagster_components.core.schema.metadata import allow_resolve, get_required_scope
77
from pydantic import BaseModel, Field, TypeAdapter, ValidationError
88

99

1010
class InnerRendered(ComponentSchemaBaseModel):
11-
a: Optional[str] = None
11+
a: Annotated[Optional[str], ResolvableFieldInfo(required_scope={"deferred"})] = None
1212

1313

1414
class Container(BaseModel):
1515
a: str
1616
inner: InnerRendered
17-
inner_scoped: Annotated[InnerRendered, ResolvableFieldInfo(additional_scope={"c", "d"})] = (
18-
Field(default_factory=InnerRendered)
17+
inner_scoped: Annotated[InnerRendered, ResolvableFieldInfo(required_scope={"c", "d"})] = Field(
18+
default_factory=InnerRendered
1919
)
2020

2121

@@ -25,36 +25,38 @@ class Outer(BaseModel):
2525
container: Container
2626
container_optional: Optional[Container] = None
2727
container_optional_scoped: Annotated[
28-
Optional[Container], ResolvableFieldInfo(additional_scope={"a", "b"})
28+
Optional[Container], ResolvableFieldInfo(required_scope={"a", "b"})
2929
] = None
3030
inner_seq: Sequence[InnerRendered]
3131
inner_optional: Optional[InnerRendered] = None
3232
inner_optional_seq: Optional[Sequence[InnerRendered]] = None
33+
transformed: Annotated[Optional[str], ResolvableFieldInfo(output_type=Optional[int])] = None
3334

3435

3536
@pytest.mark.parametrize(
3637
"path,expected",
3738
[
3839
(["a"], True),
39-
(["inner"], False),
40+
(["inner"], True),
4041
(["inner", "a"], False),
4142
(["container", "a"], True),
42-
(["container", "inner"], False),
43+
(["container", "inner"], True),
4344
(["container", "inner", "a"], False),
4445
(["container_optional", "a"], True),
45-
(["container_optional", "inner"], False),
46+
(["container_optional", "inner"], True),
4647
(["container_optional", "inner", "a"], False),
4748
(["container_optional_scoped"], False),
4849
(["container_optional_scoped", "inner", "a"], False),
4950
(["container_optional_scoped", "inner_scoped", "a"], False),
5051
(["inner_seq"], True),
51-
(["inner_seq", 0], False),
52+
(["inner_seq", 0], True),
5253
(["inner_seq", 0, "a"], False),
5354
(["inner_optional"], True),
5455
(["inner_optional", "a"], False),
5556
(["inner_optional_seq"], True),
56-
(["inner_optional_seq", 0], False),
57+
(["inner_optional_seq", 0], True),
5758
(["inner_optional_seq", 0, "a"], False),
59+
(["transformed"], False),
5860
],
5961
)
6062
def test_allow_render(path, expected: bool) -> None:
@@ -65,19 +67,17 @@ def test_allow_render(path, expected: bool) -> None:
6567
"path,expected",
6668
[
6769
(["a"], set()),
68-
(["inner", "a"], set()),
69-
(["container_optional", "inner", "a"], set()),
70+
(["inner", "a"], {"deferred"}),
71+
(["container_optional", "inner", "a"], {"deferred"}),
7072
(["inner_seq"], set()),
7173
(["container_optional_scoped"], {"a", "b"}),
7274
(["container_optional_scoped", "inner"], {"a", "b"}),
7375
(["container_optional_scoped", "inner_scoped"], {"a", "b", "c", "d"}),
74-
(["container_optional_scoped", "inner_scoped", "a"], {"a", "b", "c", "d"}),
76+
(["container_optional_scoped", "inner_scoped", "a"], {"a", "b", "c", "d", "deferred"}),
7577
],
7678
)
77-
def test_get_available_scope(path, expected: Set[str]) -> None:
78-
assert (
79-
get_available_scope(path, Outer.model_json_schema(), Outer.model_json_schema()) == expected
80-
)
79+
def test_get_required_scope(path, expected: Set[str]) -> None:
80+
assert get_required_scope(path, Outer.model_json_schema()) == expected
8181

8282

8383
def test_render() -> None:

0 commit comments

Comments
 (0)