liveBook · Manning

Dataclass

As explained in Terraform in Depth: Infrastructure as Code with Terraform and OpenTofu

A dataclass is a Python class whose common data-oriented methods and initialization behavior can be generated from declared fields.

A Python dataclass is useful when a class mainly stores and shares data. Declared fields provide its common initialization behavior, while ordinary methods can add operations that belong with that data. For a Terraform or OpenTofu command, one result object can carry the command, standard output, standard error, and return code. section 11.1.2

Listing 11.3 Command results
import json                                                   #1
from dataclasses import dataclass                             #1
from typing import Any, Generator                             #1
 
@dataclass                                                    #2
class CommandResults:
    command: str                                              #3
    stdout: str                                               #4
    stderr: str                                               #5
    returncode: int                                           #6
 
    def json(self) -> Dict[str, Any]:                         #7
        return json.loads(self.stdout)
 
    def jsonl(self) -> Generator[Dict[str, Any], None, None]: #8
        for line in self.stdout.splitlines():                 #9
            yield json.loads(line)                            #10
 
    def raise_error(self) -> None:                            #11
        try:
            for log in self.jsonl():                          #12
                if log.get("@level") == "error" and (
                    diagnostics := log.get("diagnostic")
                ):
                    raise RuntimeError(
                        f"Terraform command '{self.command}' failed: 
{diagnostics.get('summary')}\n{diagnostics.get('detail')}",
                        diagnostics,
                    )
        except Exception:                                     #13
            raise RuntimeError(
                f"Terraform command '{self.command}' failed: {self.stdout}."
            )
#1 We need to add a few new imports to use below.
#2 By turning this into a dataclass we allow Python to create methods like __init__ for us.
#3 We want to store the command to make it easier to generate useful error messages.
#4 The stdout is the most import piece, as it contains the output of the command.
#5 Most of the time we won’t need to use this, as Terraform logs to stdout in automation mode.
#6 For most commands, this records the success or failure of the command, but for Plan it has more meaning.
#7 This method will parse the stdout as JSON and return it.
#8 This method will parse the stdout as JSONL and return it.
#9 We split the stdout into lines, since JSONL is line delimited.
#10 We parse each line as JSON and yield it for use.
#11 This helper command allows us to raise an error with the command output.
#12 Some errors come in the form of JSONL, so we parse it to improve our messaging.
#13 If we can’t parse the logs as JSONL, we raise a generic error.

The CommandResults listing declares four result fields and adds methods to parse JSON or line-delimited JSON from standard output. The class keeps command data and its parsing operations together. section 11.1.2

Dataclasses can also turn command JSON into structured results. Validation uses a Validate object containing Diagnostic objects. Passing the full JSON data to each class keeps parsing near the fields it fills; the parent converts nested diagnostics into their own objects. section 11.1.5

Listing 11.11 Validate and diagnostic structures
from dataclasses import field
 
@dataclass
class Diagnostic:
    # Format: https://developer.hashicorp.com/terraform/cli/commands/
validate#json                                                      #1
    data: Dict[str, Any] = field(repr=False)                         #2
 
    severity: str = field(init=False)                                #3
    summary: str = field(init=False)                                 #3
    detail: str = field(init=False)                                  #3
 
    def __post_init__(self):                                         #4
        self.severity = self.data.get("severity")                    #5
        self.summary = self.data.get("summary")                      #6
        self.detail = self.data.get("detail")
 
 
@dataclass
class Validate:
    # Format: https://developer.hashicorp.com/terraform/cli/commands/
validate#json                                                      #7
    data: Dict[str, Any] = field(repr=False)                         #8
 
    valid: bool = field(init=False)                                  #9
    diagnostics: List[Diagnostic] = field(init=False)                #10
    error_count: int = field(init=False)                             #11
    warning_count: int = field(init=False)                           #12
 
    def __post_init__(self):
        self.valid = bool(self.data.get("valid"))                    #13
        self.diagnostics = [                                         #14
            Diagnostic(diagnostic) for diagnostic in self.data.get(
"diagnostics", [])                                                 #15
        ]
        self.error_count = int(self.data.get("error_count"))         #16
        self.warning_count = int(self.data.get("warning_count"))     #16
#1 It’s useful to store a link to the documentation for the data structure you’re using.
#2 The data field is the raw data. We don’t want it to show up if the whole object is printed.
#3 We set the init value of the field to false because we’re going to generate these values from data.
#4 This will be run immediately after the __init__ method. This is where we convert the raw data into the fields we want.
#5 The data property contains the raw data from Terraform after it has been parsed.
#6 Most of the work we’ll be doing going forward is simply mapping data to our fields.
#7 All of our classes will link to the source of their data structures.
#8 We will also always keep the raw data available in case it is needed.
#9 This lets us know if the validate passed or not.
#10 This will be a list of diagnostics that came from the validate command.
#11 The validate command returns an error count.
#12 It also returns the number of warnings.
#13 We pull in the field and convert it to the appropriate type.
#14 For nested fields like this we pass the raw data to the class we want to use.
#15 Here we are creating a list of diagnostics.
#16 We convert these fields into integers.

The Diagnostic listing accepts a data dictionary, excludes it from the representation with field(repr=False), and marks derived fields init=False. Its __post_init__ method fills those fields from the dictionary. section 11.1.5

Keep command status distinct from the result being modeled: an invalid configuration can make validation return code 1 without preventing a useful Validate object. Tests should check both successful and failed validation, including the Diagnostic objects produced for errors. section 11.1.5

How Terraform in Depth: Infrastructure as Code with Terraform and OpenTofu covers it

How deep it goes
Works through declared fields, parsing methods, nested objects, field options, and post-initialization; does not explore dataclass internals.
Examples
Uses CommandResults for command output and nested Validate and Diagnostic objects for an uninitialized workspace’s validation errors.
What it assumes
Assumes basic Python class and dictionary syntax while defining the data-sharing role of a dataclass.
What you can do after
Build it from scratch: define data-oriented result classes and convert command JSON into nested objects that callers can inspect.
How it connects
Connects dataclasses to JSON parsing, subprocess results, validation diagnostics, and post-initialization.

Also explained in

Compare how 7 books explain dataclass →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Terraform in Depth: Infrastructure as Code with Terraform and OpenTofu. The section links point to the parts of the book it draws on.