From 4486167d92dfb1bebeeb6d9c9618edc893aed2ce Mon Sep 17 00:00:00 2001 From: Giacomo Vercesi Date: Tue, 19 Jul 2022 10:09:53 +0200 Subject: [PATCH] revng.model: add TypedList Add a specialized list class to be used in the model. This will do runtime instance checking to make sure that list-like fields in the model only contain object of the correct type. --- python/revng/tupletree.py | 74 ++++++++++++++++++- .../tuple_tree_generator/generators/python.py | 10 ++- .../templates/tuple_tree_gen.py.tpl | 5 +- 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/python/revng/tupletree.py b/python/revng/tupletree.py index 6560fb8aa..d7f442503 100644 --- a/python/revng/tupletree.py +++ b/python/revng/tupletree.py @@ -3,10 +3,22 @@ # import sys +from collections.abc import MutableSequence from dataclasses import dataclass, fields from enum import Enum from functools import lru_cache -from typing import Dict, Generic, Type, TypeVar, get_args, get_origin, get_type_hints +from typing import ( + Any, + Callable, + Dict, + Generic, + List, + Type, + TypeVar, + get_args, + get_origin, + get_type_hints, +) import yaml @@ -15,7 +27,8 @@ try: from yaml import CSafeLoader as Loader except ImportError: sys.stderr.write("Warning: using the slow pure-python YAML loader and dumper!\n") - from yaml import Dumper, SafeLoader as Loader # type: ignore + from yaml import Dumper # type: ignore + from yaml import SafeLoader as Loader # type: ignore no_default = object() @@ -137,8 +150,14 @@ class StructBase: # fields as kw_only, but we want to support older python versions. # Hence this workaround, inspired by https://stackoverflow.com/a/53085935 for field in fields(self): - if self.__getattribute__(field.name) is no_default: + field_value = self.__getattribute__(field.name) + field_hints = get_type_hint_cached(self.__class__, field.name) + if field_value is no_default: raise TypeError(f"__init__ missing 1 required argument: {field.name}") + if get_origin(field_hints) is list: + new_field_value = TypedList(get_args(field_hints)[0]) + new_field_value.extend(field_value) + setattr(self, field.name, new_field_value) def __setattr__(self, key, value): # Prevent setting undefined attributes @@ -237,3 +256,52 @@ class YamlDumper(Dumper): def ignore_aliases(self, data): return True + + +class TypedList(MutableSequence): + def __init__(self, base_class: type): + self._data: List[Any] = [] + self._base_class = base_class + + def __setitem__(self, idx, obj): + if not isinstance(obj, self._base_class): + raise ValueError( + f"Cannot insert object, must be of type {self._base_class.__name__} (or subclass)" + ) + self._data[idx] = obj + + def insert(self, index: int, obj): + if not isinstance(obj, self._base_class): + raise ValueError( + f"Cannot insert object, must be of type {self._base_class.__name__} (or subclass)" + ) + self._data.insert(index, obj) + + @classmethod + def yaml_representer(cls, dumper: YamlDumper, instance) -> yaml.Node: + return dumper.represent_list(instance._data) + + def __getitem__(self, idx): + return self._data[idx] + + def __delitem__(self, idx): + del self._data[idx] + + def __len__(self) -> int: + return len(self._data) + + def __repr__(self): + return repr(self._data) + + def __str__(self): + return str(self._data) + + +YamlDumper.add_representer(TypedList, TypedList.yaml_representer) + + +def typedlist_factory(base_class: type) -> Callable[[], TypedList]: + def factory(): + return TypedList(base_class) + + return factory diff --git a/scripts/tuple_tree_generator/tuple_tree_generator/generators/python.py b/scripts/tuple_tree_generator/tuple_tree_generator/generators/python.py index dada1cbd8..a76f2a08f 100644 --- a/scripts/tuple_tree_generator/tuple_tree_generator/generators/python.py +++ b/scripts/tuple_tree_generator/tuple_tree_generator/generators/python.py @@ -4,7 +4,7 @@ import black -from ..schema import Schema, StructField +from ..schema import Schema, SequenceStructField, StructField from .jinja_utils import python_environment @@ -15,6 +15,7 @@ class PythonGenerator: self.string_types = string_types or [] self.external_types = external_types or [] python_environment.filters["python_type"] = self.python_type + python_environment.filters["python_list_type"] = self.python_list_type self.template = python_environment.get_template("tuple_tree_gen.py.tpl") def emit_python(self) -> str: @@ -41,6 +42,13 @@ class PythonGenerator: else: return f"Reference[{type_info.type}, {type_info.root_type}]" + @classmethod + def python_list_type(cls, field: SequenceStructField): + type_info = field.type_info(cls.scalar_converter) + assert type_info.root_type == "", "Must not be a Reference" + assert type_info.is_sequence, "Must be a sequence" + return type_info.type + @staticmethod def scalar_converter(type_name: str) -> str: if "int" in type_name: diff --git a/scripts/tuple_tree_generator/tuple_tree_generator/templates/tuple_tree_gen.py.tpl b/scripts/tuple_tree_generator/tuple_tree_generator/templates/tuple_tree_gen.py.tpl index 9562d5842..c954c49a0 100644 --- a/scripts/tuple_tree_generator/tuple_tree_generator/templates/tuple_tree_gen.py.tpl +++ b/scripts/tuple_tree_generator/tuple_tree_generator/templates/tuple_tree_gen.py.tpl @@ -15,6 +15,7 @@ from revng.tupletree import ( AbstractStructBase, dataclass_kwargs, no_default, + typedlist_factory, ) from revng.tupletree import YamlLoader as _ExternalYamlLoader from revng.tupletree import YamlDumper as _ExternalYamlDumper @@ -75,7 +76,7 @@ class 'struct.name'( ##- if field.is_guid -## = field(default_factory=random_id) ##- elif field is sequence_field -## - = field(default_factory=list) + = field(default_factory=typedlist_factory('field | python_list_type')) ##- elif struct.inherits -## = field(default=no_default) ##- endif ## @@ -92,7 +93,7 @@ class 'struct.name'( ## if field is simple_field ## default=None ## elif field is sequence_field ## - default_factory=list + default_factory=typedlist_factory('field | python_list_type') ## elif field is reference_field ## default=None ## endif ##