Skip to content

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

Method tags

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