API reference
The API surface is intentionally small: one class decorator, three method
tags, two exceptions, one mixin.
Class decorator
Return a class decorator that enforces strict access control.
Parameters:
| Name |
Type |
Description |
Default |
debug
|
bool
|
If True, violations are logged via the "strictaccess"
logger at WARNING level and the attribute is returned anyway.
If False (default), violations raise
:class:~strictaccess.PrivateAccessError or
:class:~strictaccess.ProtectedAccessError.
|
False
|
Source code in src/strictaccess/decorators.py
| def strict_access_control(debug: bool = False) -> Callable[[T], T]:
"""Return a class decorator that enforces strict access control.
Args:
debug: If ``True``, violations are logged via the ``"strictaccess"``
logger at ``WARNING`` level and the attribute is returned anyway.
If ``False`` (default), violations raise
:class:`~strictaccess.PrivateAccessError` or
:class:`~strictaccess.ProtectedAccessError`.
"""
def decorator(cls: T) -> T:
# ``__slots__ = ()`` on the wrapper keeps the slot layout of the
# decorated class intact. Without it, every decorated instance would
# gain a ``__dict__`` regardless of whether ``cls`` had slots.
wrapped = type(
cls.__name__,
(AccessControlMixin, cls),
{"_debug_mode": debug, "__slots__": ()},
)
# Preserve identity/introspection of the original class.
wrapped.__qualname__ = cls.__qualname__
wrapped.__module__ = cls.__module__
wrapped.__doc__ = cls.__doc__
return wrapped # type: ignore[return-value]
return decorator
|
Mark a method as private (callable only from within the defining class).
Source code in src/strictaccess/access_tags.py
| def private(func: F) -> F:
"""Mark a method as private (callable only from within the defining class)."""
func._access_level = "private" # type: ignore[attr-defined]
func._defining_class_qualname = _defining_class(func) # type: ignore[attr-defined]
return func
|
Mark a method as protected (callable from the class and its subclasses).
Source code in src/strictaccess/access_tags.py
| def protected(func: F) -> F:
"""Mark a method as protected (callable from the class and its subclasses)."""
func._access_level = "protected" # type: ignore[attr-defined]
func._defining_class_qualname = _defining_class(func) # type: ignore[attr-defined]
return func
|
Mark a method as public (callable from anywhere; bypasses name convention).
Source code in src/strictaccess/access_tags.py
| def public(func: F) -> F:
"""Mark a method as public (callable from anywhere; bypasses name convention)."""
func._access_level = "public" # type: ignore[attr-defined]
return func
|
Exceptions
Bases: AccessControlError
Raised when trying to access a private attribute or method.
Source code in src/strictaccess/exceptions.py
| class PrivateAccessError(AccessControlError):
"""Raised when trying to access a private attribute or method."""
pass
|
Bases: AccessControlError
Raised when trying to access a protected attribute or method.
Source code in src/strictaccess/exceptions.py
| class ProtectedAccessError(AccessControlError):
"""Raised when trying to access a protected attribute or method."""
pass
|
Mixin
Mixin that intercepts attribute reads to enforce access modifiers.
Source code in src/strictaccess/core.py
| class AccessControlMixin:
"""Mixin that intercepts attribute reads to enforce access modifiers."""
__slots__ = ()
_debug_mode: bool = False
def __getattribute__(self, name: str) -> Any:
# Fast path: dunder names are unconditionally allowed.
if name.startswith("__") and name.endswith("__"):
return super().__getattribute__(name)
value = super().__getattribute__(name)
# Fast path: public-named, non-callable attributes are free.
# Public-named callables still need the decorator check in case
# someone marks ``public_method`` with ``@private`` explicitly.
if not name.startswith("_") and not callable(value):
return value
# 1) Decorator-based control (callables only).
if callable(value):
level = getattr(value, "_access_level", None)
if level == "public":
return value
if level == "private":
defining = getattr(value, "_defining_class_qualname", None)
if not _caller_in_qualname(defining):
_violate(self, "private", name)
return value
if level == "protected":
if not _caller_in_mro(type(self)):
_violate(self, "protected", name)
return value
# Undecorated callable: fall through to name-based logic.
# 2) Name-based control. Public attributes (no underscore) are free.
if not name.startswith("_"):
return value
# 2a) Name-mangled ``_ClsName__x`` → private.
for cls in type(self).__mro__:
if cls is object:
continue
prefix = f"_{cls.__name__}__"
if name.startswith(prefix) and not name.endswith("__"):
if not _caller_in_qualname(cls.__qualname__):
_violate(self, "private", name)
return value
# 2b) Single underscore ``_x`` → protected by convention.
if not _caller_in_mro(type(self)):
_violate(self, "protected", name)
return value
|
__getattribute__
__getattribute__(name: str) -> Any
Source code in src/strictaccess/core.py
| def __getattribute__(self, name: str) -> Any:
# Fast path: dunder names are unconditionally allowed.
if name.startswith("__") and name.endswith("__"):
return super().__getattribute__(name)
value = super().__getattribute__(name)
# Fast path: public-named, non-callable attributes are free.
# Public-named callables still need the decorator check in case
# someone marks ``public_method`` with ``@private`` explicitly.
if not name.startswith("_") and not callable(value):
return value
# 1) Decorator-based control (callables only).
if callable(value):
level = getattr(value, "_access_level", None)
if level == "public":
return value
if level == "private":
defining = getattr(value, "_defining_class_qualname", None)
if not _caller_in_qualname(defining):
_violate(self, "private", name)
return value
if level == "protected":
if not _caller_in_mro(type(self)):
_violate(self, "protected", name)
return value
# Undecorated callable: fall through to name-based logic.
# 2) Name-based control. Public attributes (no underscore) are free.
if not name.startswith("_"):
return value
# 2a) Name-mangled ``_ClsName__x`` → private.
for cls in type(self).__mro__:
if cls is object:
continue
prefix = f"_{cls.__name__}__"
if name.startswith(prefix) and not name.endswith("__"):
if not _caller_in_qualname(cls.__qualname__):
_violate(self, "private", name)
return value
# 2b) Single underscore ``_x`` → protected by convention.
if not _caller_in_mro(type(self)):
_violate(self, "protected", name)
return value
|