diff --git a/osc/conf.py b/osc/conf.py index 07dfc0b9..779799e6 100644 --- a/osc/conf.py +++ b/osc/conf.py @@ -126,7 +126,9 @@ HttpHeader = NewType("HttpHeader", Tuple[str, str]) class OscOptions(BaseModel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.extra_fields = {} + self._allow_new_attributes = True + self._extra_fields = {} + self._allow_new_attributes = False # compat function with the config dict def _get_field_name(self, name): @@ -145,7 +147,7 @@ class OscOptions(BaseModel): field_name = self._get_field_name(name) if field_name is None and not hasattr(self, name): - return self.extra_fields[name] + return self._extra_fields[name] field_name = field_name or name try: @@ -158,7 +160,7 @@ class OscOptions(BaseModel): field_name = self._get_field_name(name) if field_name is None and not hasattr(self, name): - self.extra_fields[name] = value + self._extra_fields[name] = value return field_name = field_name or name diff --git a/osc/util/models.py b/osc/util/models.py index ab469401..5cf5ff30 100644 --- a/osc/util/models.py +++ b/osc/util/models.py @@ -280,6 +280,13 @@ class ModelMeta(type): class BaseModel(metaclass=ModelMeta): __fields__: Dict[str, Field] + def __setattr__(self, name, value): + if getattr(self, "_allow_new_attributes", True) or hasattr(self.__class__, name) or hasattr(self, name): + # allow setting properties - test if they exist in the class + # also allow setting existing attributes that were previously initialized via __dict__ + return super().__setattr__(name, value) + raise AttributeError(f"Setting attribute '{self.__class__.__name__}.{name}' is not allowed") + def __init__(self, **kwargs): self._values = {} self._parent = kwargs.pop("_parent", None) @@ -307,6 +314,8 @@ class BaseModel(metaclass=ModelMeta): for name, field in self.__fields__.items(): field.validate_type(getattr(self, name)) + self._allow_new_attributes = False + def dict(self, exclude_unset=False): result = {} for name, field in self.__fields__.items(): diff --git a/tests/test_conf.py b/tests/test_conf.py index 5d6a41d2..fa3fd7cd 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -116,6 +116,9 @@ class TestExampleConfig(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tmpdir) + def test_invalid_attribute(self): + self.assertRaises(AttributeError, setattr, self.config, "new_attribute", "123") + def test_apiurl(self): self.assertEqual(self.config["apiurl"], "https://api.opensuse.org") @@ -407,26 +410,19 @@ class TestExampleConfig(unittest.TestCase): def test_extra_fields(self): self.assertEqual(self.config["plugin-option"], "plugin-general-option") - self.assertEqual(self.config.extra_fields, {"plugin-option": "plugin-general-option"}) - - # write to an existing attribute instead of extra_fields - self.config.attrib = 123 - self.assertEqual(self.config["attrib"], 123) - self.config["attrib"] = 456 - self.assertEqual(self.config["attrib"], 456) - self.assertEqual(self.config.extra_fields, {"plugin-option": "plugin-general-option"}) + self.assertEqual(self.config._extra_fields, {"plugin-option": "plugin-general-option"}) self.config["new-option"] = "value" self.assertEqual(self.config["new-option"], "value") - self.assertEqual(self.config.extra_fields, {"plugin-option": "plugin-general-option", "new-option": "value"}) + self.assertEqual(self.config._extra_fields, {"plugin-option": "plugin-general-option", "new-option": "value"}) host_options = self.config["api_host_options"][self.config["apiurl"]] self.assertEqual(host_options["plugin-option"], "plugin-host-option") - self.assertEqual(host_options.extra_fields, {"plugin-option": "plugin-host-option"}) + self.assertEqual(host_options._extra_fields, {"plugin-option": "plugin-host-option"}) host_options["new-option"] = "value" self.assertEqual(host_options["new-option"], "value") - self.assertEqual(host_options.extra_fields, {"plugin-option": "plugin-host-option", "new-option": "value"}) + self.assertEqual(host_options._extra_fields, {"plugin-option": "plugin-host-option", "new-option": "value"}) def test_apiurl_aliases(self): expected = {"https://api.opensuse.org": "https://api.opensuse.org", "osc": "https://api.opensuse.org"}