# -*- coding: UTF-8 -*-
"""
Author: Jacek Kotlarski --<szumak@virthost.pl>
Created: 02.07.2023
Purpose: Provide helpers that restrict adding attributes to classes and instances.
The module collects mixins and metaclasses that prevent accidental creation of
new attributes at runtime. Inspired by recipes from *Python Cookbook*
(2004, Martelli et al.).
"""
from typing import Any, Callable
def _no_new_attributes(
wrapped_setattr: Any,
) -> Callable[[Any, str, Any], None]:
"""Wrap `__setattr__` to prevent creation of unknown attributes.
### Arguments:
* wrapped_setattr: Any - Original setter, e.g. `object.__setattr__` or `type.__setattr__`.
### Returns:
[Callable[[Any, str, Any], None]] - Closure enforcing attribute existence before assignment.
"""
def __setattr__(self, name: str, value: Any) -> None:
"""Delegate to the original setter when the attribute exists.
Raises AttributeError if the attribute has not been defined.
### Arguments:
* name: str - Attribute name to assign.
* value: Any - Value destined for the attribute.
"""
if hasattr(self, name):
wrapped_setattr(self, name, value)
else:
raise AttributeError(
f"Undefined attribute {name} cannot be added to {self}"
)
return __setattr__
[docs]
class NoNewAttributes:
"""Prevent instances of subclasses from gaining new attributes.
### Purpose:
Blocks dynamic attribute creation by overriding `__setattr__` for both
instances and the metaclass.
"""
__setattr__: Callable[[Any, str, Any], None] = _no_new_attributes(
object.__setattr__
)
class __metaclass__(type):
__setattr__: Callable[[Any, str, Any], None] = _no_new_attributes(
type.__setattr__
)
[docs]
class NoDynamicAttributes:
"""Mix-in that disallows adding attributes to class instances.
### Purpose:
Ensures all attributes must be declared up-front; runtime additions raise
AttributeError.
"""
[docs]
def __setattr__(self, name: str, value: Any) -> None:
"""Prevent dynamic attribute assignment on instances.
### Raises:
* AttributeError: Attribute not previously declared.
"""
if not hasattr(self, name):
raise AttributeError(
f"Cannot add new attribute '{name}' to {self.__class__.__name__} object"
)
super().__setattr__(name, value)
[docs]
class ReadOnlyClass(type):
"""Metaclass that makes class attributes immutable after definition.
### Purpose:
Raises AttributeError whenever class-level attributes are reassigned.
"""
[docs]
def __setattr__(self, name: str, value: Any) -> None:
"""Block assignment to class-level attributes.
### Raises:
* AttributeError: Always raised to prevent modification.
"""
raise AttributeError(f"Read only attribute: {name}.")
# #[EOF]#######################################################################