Skip to content

Commit 0006ebd

Browse files
committed
Refactor from_resource to no longer use generics
In the next commit we're gonna add a `to_resource` method. As we don't want to have to pass a resource into `to_resource`, the class itself needs to expose what resource class should be built. Thus a type annotation is no longer enough. To solve this we've added a class method to BaseNode which returns the associated resource class. The method on BaseNode will raise a NotImplementedError unless the the inheriting class has overridden the `resouce_class` method to return the a resource class. You may be thinking "Why not a class property"? And that is absolutely a valid question. We used to be able to chain `@classmethod` with `@property` to create a class property. However, this was deprecated in python 3.11 and removed in 3.13 (details on why this happened can be found [here](python/cpython#89519)). There is an [alternate way to setup a class property](python/cpython#89519 (comment)), however this seems a bit convoluted if a class method easily gets the job done. The draw back is that we must do `.resource_class()` instead of `.resource_class` and on classes implementing `BaseNode` we have to override it with a method instead of a property specification. Additionally, making it a class _instance_ property won't work because we don't want to require an _instance_ of the class to get the `resource_class` as we might not have an instance at our dispossal.
1 parent 1017be7 commit 0006ebd

File tree

1 file changed

+41
-16
lines changed

1 file changed

+41
-16
lines changed

core/dbt/contracts/graph/nodes.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from dataclasses import dataclass, field
55
import hashlib
66

7-
from abc import ABC
87
from mashumaro.types import SerializableType
98
from typing import (
109
Optional,
@@ -14,10 +13,9 @@
1413
Any,
1514
Sequence,
1615
Tuple,
16+
Type,
1717
Iterator,
1818
Literal,
19-
Generic,
20-
TypeVar,
2119
)
2220

2321
from dbt import deprecations
@@ -116,13 +114,19 @@
116114
# ==================================================
117115

118116

119-
ResourceTypeT = TypeVar("ResourceTypeT", bound="BaseResource")
120-
121-
122117
@dataclass
123-
class BaseNode(ABC, Generic[ResourceTypeT], BaseResource):
118+
class BaseNode(BaseResource):
124119
"""All nodes or node-like objects in this file should have this as a base class"""
125120

121+
# In an ideal world this would be a class property. However, chaining @classmethod and
122+
# @property was deprecated in python 3.11 and removed in 3.13. There are more
123+
# complicated ways of making a class property, however a class method suits our
124+
# purposes well enough
125+
@classmethod
126+
def resource_class(cls) -> Type[BaseResource]:
127+
"""Should be overriden by any class inheriting BaseNode"""
128+
raise NotImplementedError
129+
126130
@property
127131
def search_name(self):
128132
return self.name
@@ -160,12 +164,13 @@ def get_materialization(self):
160164
return self.config.materialized
161165

162166
@classmethod
163-
def from_resource(cls, resource_instance: ResourceTypeT):
167+
def from_resource(cls, resource_instance: BaseResource):
168+
assert isinstance(resource_instance, cls.resource_class())
164169
return cls.from_dict(resource_instance.to_dict())
165170

166171

167172
@dataclass
168-
class GraphNode(GraphResource, BaseNode[ResourceTypeT], Generic[ResourceTypeT]):
173+
class GraphNode(GraphResource, BaseNode):
169174
"""Nodes in the DAG. Macro and Documentation don't have fqn."""
170175

171176
def same_fqn(self, other) -> bool:
@@ -190,7 +195,7 @@ def identifier(self):
190195

191196

192197
@dataclass
193-
class ParsedNodeMandatory(GraphNode[GraphResource], HasRelationMetadata, Replaceable):
198+
class ParsedNodeMandatory(GraphNode, HasRelationMetadata, Replaceable):
194199
alias: str
195200
checksum: FileHash
196201
config: NodeConfig = field(default_factory=NodeConfig)
@@ -985,7 +990,7 @@ class UnitTestDefinitionMandatory:
985990

986991

987992
@dataclass
988-
class UnitTestDefinition(NodeInfoMixin, GraphNode[GraphResource], UnitTestDefinitionMandatory):
993+
class UnitTestDefinition(NodeInfoMixin, GraphNode, UnitTestDefinitionMandatory):
989994
description: str = ""
990995
overrides: Optional[UnitTestOverrides] = None
991996
depends_on: DependsOn = field(default_factory=DependsOn)
@@ -1211,13 +1216,17 @@ def tests(self) -> List[TestDef]:
12111216
@dataclass
12121217
class SourceDefinition(
12131218
NodeInfoMixin,
1214-
GraphNode[SourceDefinitionResource],
1219+
GraphNode,
12151220
SourceDefinitionResource,
12161221
HasRelationMetadata,
12171222
):
12181223
# Overriding the `freshness` property to use the `FreshnessThreshold` instead of the `FreshnessThresholdResource`
12191224
freshness: Optional[FreshnessThreshold] = None
12201225

1226+
@classmethod
1227+
def resource_class(cls) -> Type[SourceDefinitionResource]:
1228+
return SourceDefinitionResource
1229+
12211230
def __post_serialize__(self, dct):
12221231
if "_event_status" in dct:
12231232
del dct["_event_status"]
@@ -1326,7 +1335,7 @@ def group(self):
13261335

13271336

13281337
@dataclass
1329-
class Exposure(GraphNode[ExposureResource], ExposureResource):
1338+
class Exposure(GraphNode, ExposureResource):
13301339
@property
13311340
def depends_on_nodes(self):
13321341
return self.depends_on.nodes
@@ -1335,6 +1344,10 @@ def depends_on_nodes(self):
13351344
def search_name(self):
13361345
return self.name
13371346

1347+
@classmethod
1348+
def resource_class(cls) -> Type[ExposureResource]:
1349+
return ExposureResource
1350+
13381351
def same_depends_on(self, old: "Exposure") -> bool:
13391352
return set(self.depends_on.nodes) == set(old.depends_on.nodes)
13401353

@@ -1392,7 +1405,7 @@ def group(self):
13921405

13931406

13941407
@dataclass
1395-
class Metric(GraphNode[MetricResource], MetricResource):
1408+
class Metric(GraphNode, MetricResource):
13961409
@property
13971410
def depends_on_nodes(self):
13981411
return self.depends_on.nodes
@@ -1401,6 +1414,10 @@ def depends_on_nodes(self):
14011414
def search_name(self):
14021415
return self.name
14031416

1417+
@classmethod
1418+
def resource_class(cls) -> Type[MetricResource]:
1419+
return MetricResource
1420+
14041421
def same_description(self, old: "Metric") -> bool:
14051422
return self.description == old.description
14061423

@@ -1459,7 +1476,7 @@ class Group(GroupResource, BaseNode):
14591476

14601477

14611478
@dataclass
1462-
class SemanticModel(GraphNode[SemanticModelResource], SemanticModelResource):
1479+
class SemanticModel(GraphNode, SemanticModelResource):
14631480
@property
14641481
def depends_on_nodes(self):
14651482
return self.depends_on.nodes
@@ -1468,6 +1485,10 @@ def depends_on_nodes(self):
14681485
def depends_on_macros(self):
14691486
return self.depends_on.macros
14701487

1488+
@classmethod
1489+
def resource_class(cls) -> Type[SemanticModelResource]:
1490+
return SemanticModelResource
1491+
14711492
def same_model(self, old: "SemanticModel") -> bool:
14721493
return self.model == old.same_model
14731494

@@ -1525,7 +1546,11 @@ def same_contents(self, old: Optional["SemanticModel"]) -> bool:
15251546

15261547

15271548
@dataclass
1528-
class SavedQuery(NodeInfoMixin, GraphNode[SavedQueryResource], SavedQueryResource):
1549+
class SavedQuery(NodeInfoMixin, GraphNode, SavedQueryResource):
1550+
@classmethod
1551+
def resource_class(cls) -> Type[SavedQueryResource]:
1552+
return SavedQueryResource
1553+
15291554
def same_metrics(self, old: "SavedQuery") -> bool:
15301555
return self.query_params.metrics == old.query_params.metrics
15311556

0 commit comments

Comments
 (0)