from __future__ import annotations
from threading import Lock
from typing import List, Set, Optional, Any, Tuple
from stereotype.utils import ConfigurationError
[docs]class Role:
"""
Declares a serialization role used to exclude some fields. Role objects should usually be global variables.
Used in :class:`stereotype.Model` class method :meth:`stereotype.Model.declare_roles` to either
blacklist or whitelist that Model's fields.
:param name: A string representation of this role. Useful for custom field serialization.
:param empty_by_default: If true, this role excludes all fields by default.
"""
__slots__ = ('code', 'name', 'empty_by_default')
def __init__(self, name: str, empty_by_default: bool = False):
self.name = name
self.empty_by_default = empty_by_default
with _roles_lock:
self.code = len(_roles)
_roles.append(self)
def __repr__(self):
return f'<Role {self.name}{", empty by default" if self.empty_by_default else ""}>'
def __hash__(self):
return self.code
def __eq__(self, other):
return type(self) == type(other) and self.code == other.code
[docs] def whitelist(self, *fields, override_parents: bool = False):
"""
This role should include only the specified fields of the :class:`stereotype.Model`
:param fields: The field descriptors (``cls.field_name``) to include for this role.
:param override_parents: If true, even fields inherited from superclasses are hidden unless specified.
"""
return RequestedRoleFields(self, fields, is_whitelist=True, override_parents=override_parents)
[docs] def blacklist(self, *fields, override_parents: bool = False):
"""
This role should omit the specified fields of the :class:`stereotype.Model`.
:param fields: The field descriptors (``cls.field_name``) to omit for this role.
:param override_parents: If true, even fields inherited from superclasses are included unless specified.
"""
return RequestedRoleFields(self, fields, is_whitelist=False, override_parents=override_parents)
_roles: List[Role] = []
_roles_lock = Lock()
DEFAULT_ROLE = Role('default')
class FinalizedRoleFields:
__slots__ = ('role', 'fields')
def __init__(self, role: Role, fields: Optional[Set[str]] = None):
self.role = role
self.fields = fields or set()
def update_requested(self, other: RequestedRoleFields, all_field_names: Set[str], field_names: Set[str]):
assert self.role == other.role
if other.override_parents:
initial = set() if other.is_whitelist else all_field_names
else:
initial = self.fields
if other.is_whitelist:
self.fields = initial | other.fields
else:
self.fields = (initial | field_names) - other.fields
class RequestedRoleFields:
__slots__ = ('role', 'fields', 'is_whitelist', 'override_parents')
def __init__(self, role: Role, fields, is_whitelist: bool, override_parents: bool):
self.fields, non_descriptors = self._collect_input_fields(fields)
if non_descriptors:
raise ConfigurationError(f'Role blacklist/whitelist needs member descriptors (e.g. cls.my_field), '
f'got {non_descriptors[0]!r}')
self.role = role
self.is_whitelist = is_whitelist
self.override_parents = override_parents
@staticmethod
def _collect_input_fields(fields) -> Tuple[Set[str], List[Any]]:
field_names: Set[str] = set()
non_descriptors: List[Any] = []
for field in fields:
if type(field).__name__ == 'member_descriptor':
field_names.add(field.__name__)
elif isinstance(field, _AbstractMemberDescriptor):
field_names.add(field.name)
elif isinstance(field, property):
field_names.add(field.fget.__name__)
else:
non_descriptors.append(field)
return field_names, non_descriptors
class _AbstractMemberDescriptor:
"""Stand-in for actual member_descriptor instances in abstract classes"""
__slots__ = ['name']
def __init__(self, name: str):
self.name = name