Source code for quacks
from typing import _GenericAlias # type: ignore
from typing import TYPE_CHECKING, ClassVar, Protocol
# Single-sourcing the version number with poetry:
# https://github.com/python-poetry/poetry/pull/2366#issuecomment-652418094
__version__ = __import__("importlib.metadata").metadata.version(__name__)
__all__ = ["readonly"]
# The logic below allows the mypy plugin to be exposed at root level,
# while also ensuring:
# - mypy doesn't become a runtime dependency
# - mypy itself is not scanned during type checking
if not TYPE_CHECKING: # pragma: no cover
def __getattr__(name):
if name == "plugin":
from .mypy import plugin
return plugin
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
[docs]
def readonly(cls: type) -> type:
"""Decorate a :class:`~typing.Protocol` to make it read-only.
Unlike default protocol attributes, read-only protocols will match
frozen dataclasses and other immutable types.
Read-only attributes are already supported in protocols with
``@property``, but this is cumbersome to do for many attributes.
The ``@readonly`` decorator effectively transforms all mutable attributes
into read-only properties.
Example
-------
.. code-block:: python
from quacks import readonly
@readonly
class User(Protocol):
id: int
name: str
is_premium: bool
# equivalent to:
class User(Protocol):
@property
def id(self) -> int: ...
@property
def name(self) -> str: ...
@property
def is_premium(self) -> bool: ...
Warning
-------
Subprotocols and inherited attributes are not supported yet.
"""
if not _is_a_protocol(cls):
raise TypeError("Readonly decorator can only be applied to Protocols.")
elif any(b is not Protocol and _is_a_protocol(b) for b in cls.__bases__):
raise NotImplementedError("Subprotocols not yet supported.")
for name, typ in getattr(cls, "__annotations__", {}).items():
if not _is_classvar(typ):
@property # type: ignore
def prop(self): ... # pragma: no cover
prop.fget.__name__ = name # type: ignore
prop.fget.__annotations__ = {"return": typ} # type: ignore
setattr(cls, name, prop)
return cls
def _is_a_protocol(t: type) -> bool:
# Only classes *directly* inheriting from Protocol are protocols.
return Protocol in t.__bases__
def _is_classvar(t: type) -> bool:
return type(t) is _GenericAlias and t.__origin__ is ClassVar