> > +# Frozen makes the object immutable. This enables further optimizations, > > +# and makes it thread safe should we every want to move in that > direction. > > +@dataclass(slots=True, frozen=True) > > +class NodeConfiguration: > > + name: str > > + hostname: str > > + user: str > > + password: Optional[str] > > + > > + @staticmethod > > + def from_dict(d: dict) -> "NodeConfiguration": > > + return NodeConfiguration( > > + name=d["name"], > > + hostname=d["hostname"], > > + user=d["user"], > > + password=d.get("password"), > > + ) > > + > Out of curiosity, what is the reason for having a static "from_dict" method > rather than just a regular constructor function that takes a dict as > parameter? > @dataclass(...) is a class annotation that transforms the thing it annotates into a dataclass. This means it creates the constructor for you based on the property type annotations. If you create your own constructor, you need a constructor that can either take a single dictionary or all of the parameters like a normal constructor. Making it a static method also means that each class can manage how it should be constructed from a dictionary. Some of the other classes will transform lists or perform other assertions. It also makes it easier to have specialized types. For instance, a NICConfiguration class would have to handle all of the possible device arguments that could be passed to any PMD driver if things were passed as parameters. > > + > > +@dataclass(slots=True, frozen=True) > > +class ExecutionConfiguration: > > + system_under_test: NodeConfiguration > > + > Minor comment: seems strange having only a single member variable in this > class, effectively duplicating the class above. > More is intended to go here. For instance, what tests to run, configuration for virtual machines, the traffic generator node. > > + @staticmethod > > + def from_dict(d: dict, node_map: dict) -> "ExecutionConfiguration": > from reading the code it appears that node_map is a dict of > NodeConfiguration objects, right? Might be worth adding that to the > definition for clarity, and also the specific type of the dict "d" (if it > has one) > > + sut_name = d["system_under_test"] > > + assert sut_name in node_map, f"Unknown SUT {sut_name} in > execution {d}" > > + > > + return ExecutionConfiguration( > > + system_under_test=node_map[sut_name], > > + ) > > + > > + > > +@dataclass(slots=True, frozen=True) > > +class Configuration: > > + executions: list[ExecutionConfiguration] > > + > > + @staticmethod > > + def from_dict(d: dict) -> "Configuration": > > + nodes: list[NodeConfiguration] = list( > > + map(NodeConfiguration.from_dict, d["nodes"]) > So "d" is a dict of dicts? > d is a dictionary which matches the json schema for the class. In the case of the Configuration class, it is a dictionary matching the entire json schema. > > + ) > > + assert len(nodes) > 0, "There must be a node to test" > > + > > + node_map = {node.name: node for node in nodes} > > + assert len(nodes) == len(node_map), "Duplicate node names are > not allowed" > > + > > + executions: list[ExecutionConfiguration] = list( > > + map( > > + ExecutionConfiguration.from_dict, d["executions"], > [node_map for _ in d] > > + ) > > + ) > > + > > + return Configuration(executions=executions) > > + > > + > > +def load_config() -> Configuration: > > + """ > > + Loads the configuration file and the configuration file schema, > > + validates the configuration file, and creates a configuration > object. > > + """ > > + with open(SETTINGS.config_file_path, "r") as f: > > + config_data = yaml.safe_load(f) > > + > > + schema_path = os.path.join( > > + pathlib.Path(__file__).parent.resolve(), "conf_yaml_schema.json" > > + ) > > + > > + with open(schema_path, "r") as f: > > + schema = json.load(f) > > + config: dict[str, Any] = warlock.model_factory(schema, > name="_Config")(config_data) > > + config_obj: Configuration = Configuration.from_dict(dict(config)) > > + return config_obj > > + > > + > > +CONFIGURATION = load_config() >