I have a root-level config
class that I pass throughout my codebase through dependency injection.
Problem is, I have this dataclass that needs some attributes from this config to calculate a value world_coords
in __post_init__()
.
In order to keep my tests clean and to circumvent other test import issues (not discussed here), I want to be able to pass in the config directly to this object instead of reading these values from an import. If I structure the config as an argument, however, it becomes an attribute of the dataclass, which I'm trying to avoid. There isn't really a need for RoadSegmentNode
to hold on to a reference of the config once used.
Here's the basic structure of the class:
@dataclass(eq=True, frozen=True) # make hashable
class RoadSegmentNode:
tile_index: Tuple[int, int] # (r, c)
dir: Direction
node_type: RoadNodeType
world_coords: Tuple[int, int] = field(init=False)
def __post_init__(self):
# (Use config variables here, e.g. `config.TILE_WIDTH`, to calculate x and y)
# Hack to get around frozen=True. We don't care that we're mutating
# an "immutable" object on __init__().
object.__setattr__(self, "world_coords", (x, y))
Here's the stopgap I went with to keep with the dependency injection model to unblock my tests for now. Note how RoadSegmentNode
now has a bunch of new attributes that would only use for initialization. It's a little better than keeping a reference to the config, because at least they're explicit, but it's still a pretty poor design.
@dataclass(eq=True, frozen=True) # make hashable
class RoadSegmentNode:
# NOTE: DO NOT ACCESS THESE ATTRIBUTES!
grid_width: int
grid_height: int
tile_width: int
tile_height: int
road_width: int
tile_index: Tuple[int, int] # (r, c)
dir: Direction
node_type: RoadNodeType
world_coords: Tuple[int, int] = field(init=False)
def __post_init__(self):
# (Use attributes here, e.g. `self.tile_width`, to calculate x and y)
# Hack to get around frozen=True. We don't care that we're mutating
# an "immutable" object on __init__().
object.__setattr__(self, "world_coords", (x, y))
How can I pass the config to the dataclass for initialization without making it an attribute of the dataclass? Should I even be considering a dataclass for this use case? I believe the original intention was to keep all instances immutable, but I can't confirm.
You should define config
as an init-only variable. This way it'll be passed to __post_init__()
, but will disappear after that:
from dataclasses import dataclass, field, InitVar
@dataclass(eq=True, frozen=True)
class RoadSegmentNode:
tile_index: Tuple[int, int]
dir: Direction
node_type: RoadNodeType
world_coords: Tuple[int, int] = field(init=False)
config: InitVar[Config] # will not appear in instances
def __post_init__(self, config):
x, y = ..., ... # calculate using config
# looks hacky for sure, but is the right way to work with frozen dataclasses
object.__setattr__(self, "world_coords", (x, y))