Skip to content

Commit 07fa58f

Browse files
authored
[component] remove DefsModuleComponent (#29189)
The wrapped components were confusing me so I took a spin at refactoring to try to simplify things. `get_component()` for (maybe) getting a `Component` at `context.path` `DefsFolderComponent.get()` for expecting to get a `DefsFolderComponent` like for `defs` folder ## How I Tested These Changes existing coverage
1 parent 00d64dd commit 07fa58f

File tree

4 files changed

+114
-173
lines changed

4 files changed

+114
-173
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import inspect
2-
from abc import abstractmethod
3-
from collections.abc import Mapping, Sequence
2+
from collections.abc import Iterator, Mapping, Sequence
43
from dataclasses import dataclass
54
from pathlib import Path
65
from typing import Any, Optional, TypeVar
@@ -45,34 +44,33 @@ class ComponentFileModel(BaseModel):
4544
requirements: Optional[ComponentRequirementsModel] = None
4645

4746

48-
#########
49-
# MODULES
50-
#########
51-
52-
53-
@dataclass
54-
class DefsModuleComponent(Component):
55-
path: Path
56-
57-
@classmethod
58-
def from_context(cls, context: ComponentLoadContext) -> Optional["DefsModuleComponent"]:
59-
"""Attempts to load a component from the given context. Iterates through potential component
60-
type matches, prioritizing more specific types: YAML, Python, plain Dagster defs, and component
61-
folder.
62-
"""
63-
# this defines the priority of the module types
64-
module_types = (
65-
WrappedYamlComponent,
66-
WrappedPythonicComponent,
67-
DagsterDefsComponent,
68-
DefsFolderComponent,
47+
def get_component(context: ComponentLoadContext) -> Optional[Component]:
48+
"""Attempts to load a component from the given context. Iterates through potential component
49+
type matches, prioritizing more specific types: YAML, Python, plain Dagster defs, and component
50+
folder.
51+
"""
52+
# in priority order
53+
# yaml component
54+
if (context.path / "component.yaml").exists():
55+
return load_yaml_component(context)
56+
# pythonic component
57+
elif (context.path / "component.py").exists():
58+
return load_pythonic_component(context)
59+
# defs
60+
elif (context.path / "definitions.py").exists():
61+
return DagsterDefsComponent(path=context.path / "definitions.py")
62+
elif context.path.suffix == ".py":
63+
return DagsterDefsComponent(path=context.path)
64+
# folder
65+
elif context.path.is_dir():
66+
children = _crawl(context)
67+
return DefsFolderComponent(
68+
path=context.path,
69+
children=children,
70+
asset_post_processors=None,
6971
)
70-
module_filter = filter(None, (cls.from_context(context) for cls in module_types))
71-
return next(module_filter, None)
7272

73-
@classmethod
74-
def load(cls, attributes: Any, context: ComponentLoadContext) -> "DefsModuleComponent":
75-
return check.not_none(cls.from_context(context))
73+
return None
7674

7775

7876
@dataclass
@@ -81,15 +79,16 @@ class DefsFolderComponentYamlSchema(Resolvable):
8179

8280

8381
@dataclass
84-
class DefsFolderComponent(DefsModuleComponent):
82+
class DefsFolderComponent(Component):
8583
"""A folder which may contain multiple submodules, each
8684
which define components.
8785
8886
Optionally enables postprocessing to modify the Dagster definitions
8987
produced by submodules.
9088
"""
9189

92-
submodules: Sequence[DefsModuleComponent]
90+
path: Path
91+
children: Mapping[Path, Component]
9392
asset_post_processors: Optional[Sequence[AssetPostProcessor]]
9493

9594
@classmethod
@@ -105,59 +104,54 @@ def load(cls, attributes: Any, context: ComponentLoadContext) -> "DefsFolderComp
105104
context.resolution_context.at_path("attributes"),
106105
attributes,
107106
)
108-
submodules = check.not_none(cls.submodules_from_context(context))
107+
109108
return DefsFolderComponent(
110109
path=context.path,
111-
submodules=submodules,
110+
children=_crawl(context),
112111
asset_post_processors=resolved_attributes.asset_post_processors,
113112
)
114113

115-
@classmethod
116-
def submodules_from_context(
117-
cls, context: ComponentLoadContext
118-
) -> Optional[Sequence[DefsModuleComponent]]:
119-
if not context.path.is_dir():
120-
return None
121-
submodules = (
122-
DefsModuleComponent.from_context(context.for_path(subpath))
123-
for subpath in context.path.iterdir()
124-
)
125-
return list(filter(None, submodules))
126-
127-
@classmethod
128-
def from_context(cls, context: ComponentLoadContext) -> Optional["DefsFolderComponent"]:
129-
submodules = cls.submodules_from_context(context)
130-
if submodules is None:
131-
return None
132-
return DefsFolderComponent(
133-
path=context.path, submodules=submodules, asset_post_processors=None
134-
)
135-
136114
def build_defs(self, context: ComponentLoadContext) -> Definitions:
137115
defs = Definitions.merge(
138-
*(
139-
submodule.build_defs(context.for_path(submodule.path))
140-
for submodule in self.submodules
141-
)
116+
*(child.build_defs(context.for_path(path)) for path, child in self.children.items())
142117
)
143118
for post_processor in self.asset_post_processors or []:
144119
defs = post_processor(defs)
145120
return defs
146121

122+
@classmethod
123+
def get(cls, context: ComponentLoadContext) -> "DefsFolderComponent":
124+
component = get_component(context)
125+
return check.inst(
126+
component,
127+
DefsFolderComponent,
128+
f"Expected DefsFolderComponent at {context.path}, got {component}.",
129+
)
130+
131+
def iterate_components(self) -> Iterator[Component]:
132+
for component in self.children.values():
133+
if isinstance(component, DefsFolderComponent):
134+
yield from component.iterate_components()
135+
136+
yield component
137+
138+
139+
def _crawl(context: ComponentLoadContext) -> Mapping[Path, Component]:
140+
found = {}
141+
for subpath in context.path.iterdir():
142+
component = get_component(context.for_path(subpath))
143+
if component:
144+
found[subpath] = component
145+
return found
146+
147147

148148
@dataclass
149-
class DagsterDefsComponent(DefsModuleComponent):
149+
class DagsterDefsComponent(Component):
150150
"""A Python module containing Dagster definitions. Used for implicit loading of
151151
Dagster definitions from Python files in the defs folder.
152152
"""
153153

154-
@classmethod
155-
def from_context(cls, context: ComponentLoadContext) -> Optional["DagsterDefsComponent"]:
156-
if (context.path / "definitions.py").exists():
157-
return DagsterDefsComponent(path=context.path / "definitions.py")
158-
elif context.path.suffix == ".py":
159-
return DagsterDefsComponent(path=context.path)
160-
return None
154+
path: Path
161155

162156
def build_defs(self, context: ComponentLoadContext) -> Definitions:
163157
module = context.load_defs_relative_python_module(self.path)
@@ -173,103 +167,57 @@ def build_defs(self, context: ComponentLoadContext) -> Definitions:
173167
)
174168

175169

176-
@dataclass
177-
class WrappedDefsModuleComponent(DefsModuleComponent):
178-
"""A module containing a component definition. ABC implemented by
179-
WrappedPythonicComponent and WrappedYamlComponent.
180-
"""
181-
182-
wrapped: Component
183-
184-
@staticmethod
185-
@abstractmethod
186-
def get_component_def_path(path: Path) -> Path: ...
187-
188-
@classmethod
189-
@abstractmethod
190-
def get_component(cls, context: ComponentLoadContext) -> Component: ...
191-
192-
@classmethod
193-
def from_context(cls, context: ComponentLoadContext) -> Optional["WrappedDefsModuleComponent"]:
194-
if cls.get_component_def_path(context.path).exists():
195-
return cls(path=context.path, wrapped=cls.get_component(context))
196-
return None
197-
198-
def build_defs(self, context: ComponentLoadContext) -> Definitions:
199-
return self.wrapped.build_defs(context)
200-
201-
202-
@dataclass
203-
class WrappedPythonicComponent(WrappedDefsModuleComponent):
204-
"""A module which contains a component definition specified in Python
205-
with an @component decorator.
206-
"""
207-
208-
@staticmethod
209-
def get_component_def_path(path: Path) -> Path:
210-
return path / "component.py"
211-
212-
@classmethod
213-
def get_component(cls, context) -> Component:
214-
module = context.load_defs_relative_python_module(cls.get_component_def_path(context.path))
215-
component_loaders = list(inspect.getmembers(module, is_component_loader))
216-
if len(component_loaders) == 0:
217-
raise DagsterInvalidDefinitionError("No component loaders found in module")
218-
elif len(component_loaders) == 1:
219-
_, component_loader = component_loaders[0]
220-
return WrappedPythonicComponent(path=context.path, wrapped=component_loader(context))
221-
else:
222-
raise DagsterInvalidDefinitionError(
223-
f"Multiple component loaders found in module: {component_loaders}"
224-
)
225-
226-
227-
@dataclass
228-
class WrappedYamlComponent(WrappedDefsModuleComponent):
229-
"""A module containing a component definition specified in a component.yaml file."""
170+
def load_pythonic_component(context: ComponentLoadContext) -> Component:
171+
module = context.load_defs_relative_python_module(context.path / "component.py")
172+
component_loaders = list(inspect.getmembers(module, is_component_loader))
173+
if len(component_loaders) == 0:
174+
raise DagsterInvalidDefinitionError("No component loaders found in module")
175+
elif len(component_loaders) == 1:
176+
_, component_loader = component_loaders[0]
177+
return component_loader(context)
178+
else:
179+
raise DagsterInvalidDefinitionError(
180+
f"Multiple component loaders found in module: {component_loaders}"
181+
)
230182

231-
@staticmethod
232-
def get_component_def_path(path: Path) -> Path:
233-
return path / "component.yaml"
234183

235-
@classmethod
236-
def get_component(cls, context: ComponentLoadContext) -> Component:
237-
# parse the yaml file
238-
component_def_path = cls.get_component_def_path(context.path)
239-
source_tree = parse_yaml_with_source_positions(
240-
component_def_path.read_text(), str(component_def_path)
241-
)
242-
component_file_model = _parse_and_populate_model_with_annotated_errors(
243-
cls=ComponentFileModel, obj_parse_root=source_tree, obj_key_path_prefix=[]
184+
def load_yaml_component(context: ComponentLoadContext) -> Component:
185+
# parse the yaml file
186+
component_def_path = context.path / "component.yaml"
187+
source_tree = parse_yaml_with_source_positions(
188+
component_def_path.read_text(), str(component_def_path)
189+
)
190+
component_file_model = _parse_and_populate_model_with_annotated_errors(
191+
cls=ComponentFileModel, obj_parse_root=source_tree, obj_key_path_prefix=[]
192+
)
193+
194+
# find the component type
195+
type_str = context.normalize_component_type_str(component_file_model.type)
196+
key = PluginObjectKey.from_typename(type_str)
197+
obj = load_package_object(key)
198+
if not isinstance(obj, type) or not issubclass(obj, Component):
199+
raise DagsterInvalidDefinitionError(
200+
f"Component type {type_str} is of type {type(obj)}, but must be a subclass of dagster.Component"
244201
)
245202

246-
# find the component type
247-
type_str = context.normalize_component_type_str(component_file_model.type)
248-
key = PluginObjectKey.from_typename(type_str)
249-
obj = load_package_object(key)
250-
if not isinstance(obj, type) or not issubclass(obj, Component):
251-
raise DagsterInvalidDefinitionError(
252-
f"Component type {type_str} is of type {type(obj)}, but must be a subclass of dagster.Component"
253-
)
254-
255-
model_cls = obj.get_model_cls()
256-
context = context.with_rendering_scope(
257-
obj.get_additional_scope()
258-
).with_source_position_tree(source_tree.source_position_tree)
259-
260-
# grab the attributes from the yaml file
261-
with pushd(str(context.path)):
262-
if model_cls is None:
263-
attributes = None
264-
elif source_tree:
265-
attributes_position_tree = source_tree.source_position_tree.children["attributes"]
266-
with enrich_validation_errors_with_source_position(
267-
attributes_position_tree, ["attributes"]
268-
):
269-
attributes = TypeAdapter(model_cls).validate_python(
270-
component_file_model.attributes
271-
)
272-
else:
203+
model_cls = obj.get_model_cls()
204+
context = context.with_rendering_scope(
205+
obj.get_additional_scope(),
206+
).with_source_position_tree(
207+
source_tree.source_position_tree,
208+
)
209+
210+
# grab the attributes from the yaml file
211+
with pushd(str(context.path)):
212+
if model_cls is None:
213+
attributes = None
214+
elif source_tree:
215+
attributes_position_tree = source_tree.source_position_tree.children["attributes"]
216+
with enrich_validation_errors_with_source_position(
217+
attributes_position_tree, ["attributes"]
218+
):
273219
attributes = TypeAdapter(model_cls).validate_python(component_file_model.attributes)
220+
else:
221+
attributes = TypeAdapter(model_cls).validate_python(component_file_model.attributes)
274222

275-
return obj.load(attributes, context)
223+
return obj.load(attributes, context)

python_modules/dagster/dagster/components/core/load_defs.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ def load_defs(defs_root: ModuleType) -> Definitions:
3939
resources (Optional[Mapping[str, object]]): A mapping of resource keys to resources
4040
to apply to the definitions.
4141
"""
42-
from dagster.components.core.defs_module import DefsModuleComponent
42+
from dagster.components.core.defs_module import get_component
4343
from dagster.components.core.package_entry import discover_entry_point_package_objects
4444
from dagster.components.core.snapshot import get_package_entry_snap
4545

4646
# create a top-level DefsModule component from the root module
4747
context = ComponentLoadContext.for_module(defs_root)
48-
root_component = DefsModuleComponent.from_context(context)
48+
root_component = get_component(context)
4949
if root_component is None:
5050
raise DagsterInvalidDefinitionError("Could not resolve root module to a component.")
5151

python_modules/dagster/dagster/components/lib/definitions_component/__init__.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import Optional
33

44
from dagster._core.definitions.definitions_class import Definitions
5-
from dagster._core.errors import DagsterInvalidDefinitionError
65
from dagster.components.component.component import Component
76
from dagster.components.core.context import ComponentLoadContext
87
from dagster.components.core.defs_module import DagsterDefsComponent
@@ -20,10 +19,5 @@ class DefinitionsComponent(Component, Model, Resolvable):
2019
path: Optional[str]
2120

2221
def build_defs(self, context: ComponentLoadContext) -> Definitions:
23-
inner_context = context.for_path(Path(self.path)) if self.path else context
24-
component = DagsterDefsComponent.from_context(inner_context)
25-
if component is None:
26-
raise DagsterInvalidDefinitionError(
27-
f"Could not resolve {self.path} to a DagsterDefsComponent."
28-
)
22+
component = DagsterDefsComponent(Path(self.path) if self.path else context.path)
2923
return component.build_defs(context)

python_modules/libraries/dagster-sling/dagster_sling_tests/test_sling_replication_collection_component.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from dagster._utils.env import environ
2323
from dagster.components import ComponentLoadContext
2424
from dagster.components.cli import cli
25-
from dagster.components.core.defs_module import DefsModuleComponent, WrappedYamlComponent
25+
from dagster.components.core.defs_module import get_component
2626
from dagster.components.resolved.context import ResolutionException
2727
from dagster.components.resolved.core_models import AssetAttributesModel
2828
from dagster_sling import SlingReplicationCollectionComponent, SlingResource
@@ -80,10 +80,9 @@ def temp_sling_component_instance(
8080
data["attributes"]["sling"]["connections"][0]["instance"] = f"{temp_dir}/duckdb"
8181

8282
context = ComponentLoadContext.for_test().for_path(component_path)
83-
defs_module = DefsModuleComponent.from_context(context)
84-
assert isinstance(defs_module, WrappedYamlComponent)
85-
assert isinstance(defs_module.wrapped, SlingReplicationCollectionComponent)
86-
yield defs_module.wrapped, defs_module.build_defs(context)
83+
component = get_component(context)
84+
assert isinstance(component, SlingReplicationCollectionComponent)
85+
yield component, component.build_defs(context)
8786

8887

8988
def test_python_attributes() -> None:

0 commit comments

Comments
 (0)