Skip to content

typing

frequenz.core.typing ¤

Type hints and utility functions for type checking and types.

This module provides a decorator to disable the __init__ constructor of a class, to force the use of a factory method to create instances. See @disable_init for more information.

It also provides a metaclass used by the decorator to disable the __init__: NoInitConstructibleMeta. This is useful mostly for disabling __init__ while having to use another metaclass too (like abc.ABCMeta).

Attributes¤

frequenz.core.typing.TypeT module-attribute ¤

TypeT = TypeVar('TypeT', bound=type)

A type variable that is bound to a type.

Classes¤

frequenz.core.typing.NoInitConstructibleMeta ¤

Bases: type

A metaclass that disables the __init__ constructor.

This metaclass can be used to disable the __init__ constructor of a class. It is intended to be used with classes that don't provide a default constructor and require the use of a factory method to create instances.

When marking a class using this metaclass, the class cannot be even declared with a __init__ method, as it will raise a TypeError when the class is created, as soon as the class is parsed by the Python interpreter. It will also raise a TypeError when the __init__ method is called.

To create an instance you must provide a factory method, using __new__.

Warning

It is also recommended to apply this metaclass only to classes inheriting from object directly (i.e. not explicitly inheriting from any other classes), as things can also get tricky when applying the constructor to a sub-class for the first time.

Basic example defining a class with a factory method

To be able to type hint the class correctly, you can declare the instance attributes in the class body, and then use a factory method to create instances.

from typing import Self

class MyClass(metaclass=NoInitConstructibleMeta):
    value: int

    @classmethod
    def new(cls, value: int = 1) -> Self:
        self = cls.__new__(cls)
        self.value = value
        return self

instance = MyClass.new()

# Calling the default constructor (__init__) will raise a TypeError
try:
    instance = MyClass()
except TypeError as e:
    print(e)

Hint: The @disable_init decorator is a more convenient way to use this metaclass.

Example combining with other metaclass

A typical case where you might want this is to combine with abc.ABCMeta to create an abstract class that doesn't provide a default constructor.

from abc import ABCMeta, abstractmethod
from typing import Self

class NoInitConstructibleABCMeta(ABCMeta, NoInitConstructibleMeta):
    pass

class MyAbstractClass(metaclas=NoInitConstructibleABCMeta):
    @abstractmethod
    def do_something(self) -> None:
        ...

class MyClass(MyAbstractClass):
    value: int

    @classmethod
    def new(cls, value: int = 1) -> Self:
        self = cls.__new__(cls)
        self.value = value
        return self

    def do_something(self) -> None:
        print("Doing something")

instance = MyClass.new()
instance.do_something()

# Calling the default constructor (__init__) will raise a TypeError
try:
    instance = MyClass()
except TypeError as e:
    print(e)
Source code in frequenz/core/typing.py
class NoInitConstructibleMeta(type):
    """A metaclass that disables the `__init__` constructor.

    This metaclass can be used to disable the `__init__` constructor of a class. It is
    intended to be used with classes that don't provide a default constructor and
    require the use of a factory method to create instances.

    When marking a class using this metaclass, the class cannot be even declared with a
    `__init__` method, as it will raise a `TypeError` when the class is created, as soon
    as the class is parsed by the Python interpreter. It will also raise a `TypeError`
    when the `__init__` method is called.

    To create an instance you must provide a factory method, using `__new__`.

    Warning:
        It is also recommended to apply this metaclass only to classes inheriting from
        `object` directly (i.e. not explicitly inheriting from any other classes), as
        things can also get tricky when applying the constructor to a sub-class for the
        first time.

    Example: Basic example defining a class with a factory method
        To be able to type hint the class correctly, you can declare the instance
        attributes in the class body, and then use a factory method to create instances.

        ```python
        from typing import Self

        class MyClass(metaclass=NoInitConstructibleMeta):
            value: int

            @classmethod
            def new(cls, value: int = 1) -> Self:
                self = cls.__new__(cls)
                self.value = value
                return self

        instance = MyClass.new()

        # Calling the default constructor (__init__) will raise a TypeError
        try:
            instance = MyClass()
        except TypeError as e:
            print(e)
        ```

        Hint:
            The [`@disable_init`][frequenz.core.typing.disable_init] decorator is a more
            convenient way to use this metaclass.

    Example: Example combining with other metaclass
        A typical case where you might want this is to combine with
        [`abc.ABCMeta`][abc.ABCMeta] to create an abstract class that doesn't provide a
        default constructor.

        ```python
        from abc import ABCMeta, abstractmethod
        from typing import Self

        class NoInitConstructibleABCMeta(ABCMeta, NoInitConstructibleMeta):
            pass

        class MyAbstractClass(metaclas=NoInitConstructibleABCMeta):
            @abstractmethod
            def do_something(self) -> None:
                ...

        class MyClass(MyAbstractClass):
            value: int

            @classmethod
            def new(cls, value: int = 1) -> Self:
                self = cls.__new__(cls)
                self.value = value
                return self

            def do_something(self) -> None:
                print("Doing something")

        instance = MyClass.new()
        instance.do_something()

        # Calling the default constructor (__init__) will raise a TypeError
        try:
            instance = MyClass()
        except TypeError as e:
            print(e)
        ```

    """

    # We need to use noqa here because pydoclint can't figure out that
    # _get_no_init_constructible_error() returns a TypeError.
    def __new__(  # noqa: DOC503
        mcs,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        **kwargs: Any,
    ) -> type:
        """Create a new class with a disabled __init__ constructor.

        Args:
            name: The name of the new class.
            bases: The base classes of the new class.
            namespace: The namespace of the new class.
            **kwargs: Additional keyword arguments.

        Returns:
            The new class with a disabled __init__ constructor.

        Raises:
            TypeError: If the class provides a default constructor.
        """
        if "__init__" in namespace:
            raise _get_no_init_constructible_error(name, bases, **kwargs)
        return super().__new__(mcs, name, bases, namespace)

    def __init__(
        cls,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        **kwargs: Any,
    ) -> None:
        """Initialize the new class."""
        super().__init__(name, bases, namespace)
        cls._no_init_constructible_error = kwargs.get("no_init_constructible_error")

    # We need to use noqa here because pydoclint can't figure out that
    # _get_no_init_constructible_error() returns a TypeError.
    def __call__(cls, *args: Any, **kwargs: Any) -> NoReturn:  # noqa: DOC503
        """Raise an error when the __init__ constructor is called.

        Args:
            *args: ignored positional arguments.
            **kwargs: ignored keyword arguments.

        Raises:
            TypeError: Always.
        """
        raise _get_no_init_constructible_error(
            cls.__name__,
            cls.__bases__,
            no_init_constructible_error=cls._no_init_constructible_error,
        )
Functions¤
__call__ ¤
__call__(*args: Any, **kwargs: Any) -> NoReturn

Raise an error when the init constructor is called.

PARAMETER DESCRIPTION
*args

ignored positional arguments.

TYPE: Any DEFAULT: ()

**kwargs

ignored keyword arguments.

TYPE: Any DEFAULT: {}

RAISES DESCRIPTION
TypeError

Always.

Source code in frequenz/core/typing.py
def __call__(cls, *args: Any, **kwargs: Any) -> NoReturn:  # noqa: DOC503
    """Raise an error when the __init__ constructor is called.

    Args:
        *args: ignored positional arguments.
        **kwargs: ignored keyword arguments.

    Raises:
        TypeError: Always.
    """
    raise _get_no_init_constructible_error(
        cls.__name__,
        cls.__bases__,
        no_init_constructible_error=cls._no_init_constructible_error,
    )
__init__ ¤
__init__(
    name: str,
    bases: tuple[type, ...],
    namespace: dict[str, Any],
    **kwargs: Any
) -> None

Initialize the new class.

Source code in frequenz/core/typing.py
def __init__(
    cls,
    name: str,
    bases: tuple[type, ...],
    namespace: dict[str, Any],
    **kwargs: Any,
) -> None:
    """Initialize the new class."""
    super().__init__(name, bases, namespace)
    cls._no_init_constructible_error = kwargs.get("no_init_constructible_error")
__new__ ¤
__new__(
    mcs,
    name: str,
    bases: tuple[type, ...],
    namespace: dict[str, Any],
    **kwargs: Any
) -> type

Create a new class with a disabled init constructor.

PARAMETER DESCRIPTION
name

The name of the new class.

TYPE: str

bases

The base classes of the new class.

TYPE: tuple[type, ...]

namespace

The namespace of the new class.

TYPE: dict[str, Any]

**kwargs

Additional keyword arguments.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
type

The new class with a disabled init constructor.

RAISES DESCRIPTION
TypeError

If the class provides a default constructor.

Source code in frequenz/core/typing.py
def __new__(  # noqa: DOC503
    mcs,
    name: str,
    bases: tuple[type, ...],
    namespace: dict[str, Any],
    **kwargs: Any,
) -> type:
    """Create a new class with a disabled __init__ constructor.

    Args:
        name: The name of the new class.
        bases: The base classes of the new class.
        namespace: The namespace of the new class.
        **kwargs: Additional keyword arguments.

    Returns:
        The new class with a disabled __init__ constructor.

    Raises:
        TypeError: If the class provides a default constructor.
    """
    if "__init__" in namespace:
        raise _get_no_init_constructible_error(name, bases, **kwargs)
    return super().__new__(mcs, name, bases, namespace)

Functions¤

frequenz.core.typing.disable_init ¤

disable_init(
    cls: None = None, *, error: Exception | None = None
) -> Callable[[TypeT], TypeT]
disable_init(cls: TypeT) -> TypeT
disable_init(
    cls: TypeT | None = None,
    *,
    error: Exception | None = None
) -> TypeT | Callable[[TypeT], TypeT]

Disable the __init__ constructor of a class.

This decorator can be used to disable the __init__ constructor of a class. It is intended to be used with classes that don't provide a default constructor and require the use of a factory method to create instances.

When marking a class with this decorator, the class cannot be even declared with a __init__ method, as it will raise a TypeError when the class is created, as soon as the class is parsed by the Python interpreter. It will also raise a TypeError when the __init__ method is called.

To create an instance you must provide a factory method, using __new__.

Warning

This decorator will use a custom metaclass to disable the __init__ constructor of the class, so if your class already uses a custom metaclass, you should be aware of potential conflicts. See NoInitConstructibleMeta for an example on how to use more than one metaclass.

It is also recommended to apply this decorator only to classes inheriting from object directly (i.e. not explicitly inheriting from any other classes), as things can also get tricky when applying the constructor to a sub-class for the first time.

Basic example defining a class with a factory method

To be able to type hint the class correctly, you can declare the instance attributes in the class body, and then use a factory method to create instances.

from typing import Self

@disable_init
class MyClass:
    value: int

    @classmethod
    def new(cls, value: int = 1) -> Self:
        self = cls.__new__(cls)
        self.value = value
        return self

instance = MyClass.new()

# Calling the default constructor (__init__) will raise a TypeError
try:
    instance = MyClass()
except TypeError as e:
    print(e)
Class wrongly providing an __init__ constructor
try:
    @disable_init
    class MyClass:
        def __init__(self) -> None:
            pass
except TypeError as e:
    assert isinstance(e, TypeError)
    print(e)
Using a custom error message when the default constructor is called
from typing import Self

class NoInitError(TypeError):
    def __init__(self) -> None:
        super().__init__("Please create instances of MyClass using MyClass.new()")

@disable_init(error=NoInitError())
class MyClass:
    @classmethod
    def new(cls) -> Self:
        return cls.__new__(cls)

try:
    instance = MyClass()
except NoInitError as e:
    assert str(e) == "Please create instances of MyClass using MyClass.new()"
    print(e)
PARAMETER DESCRIPTION
cls

The class to be decorated.

TYPE: TypeT | None DEFAULT: None

error

The error to raise if init is called, if None a default TypeError will be raised.

TYPE: Exception | None DEFAULT: None

RETURNS DESCRIPTION
TypeT | Callable[[TypeT], TypeT]

A decorator that disables the __init__ constructor of cls.

Source code in frequenz/core/typing.py
def disable_init(
    cls: TypeT | None = None,
    *,
    error: Exception | None = None,
) -> TypeT | Callable[[TypeT], TypeT]:
    """Disable the `__init__` constructor of a class.

    This decorator can be used to disable the `__init__` constructor of a class. It is
    intended to be used with classes that don't provide a default constructor and
    require the use of a factory method to create instances.

    When marking a class with this decorator, the class cannot be even declared with a
    `__init__` method, as it will raise a `TypeError` when the class is created, as soon
    as the class is parsed by the Python interpreter. It will also raise a `TypeError`
    when the `__init__` method is called.

    To create an instance you must provide a factory method, using `__new__`.

    Warning:
        This decorator will use a custom metaclass to disable the `__init__` constructor
        of the class, so if your class already uses a custom metaclass, you should be
        aware of potential conflicts. See
        [`NoInitConstructibleMeta`][frequenz.core.typing.NoInitConstructibleMeta] for an
        example on how to use more than one metaclass.

        It is also recommended to apply this decorator only to classes inheriting from
        `object` directly (i.e. not explicitly inheriting from any other classes), as
        things can also get tricky when applying the constructor to a sub-class for the
        first time.

    Example: Basic example defining a class with a factory method
        To be able to type hint the class correctly, you can declare the instance
        attributes in the class body, and then use a factory method to create instances.

        ```python
        from typing import Self

        @disable_init
        class MyClass:
            value: int

            @classmethod
            def new(cls, value: int = 1) -> Self:
                self = cls.__new__(cls)
                self.value = value
                return self

        instance = MyClass.new()

        # Calling the default constructor (__init__) will raise a TypeError
        try:
            instance = MyClass()
        except TypeError as e:
            print(e)
        ```

    Example: Class wrongly providing an `__init__` constructor
        ```python
        try:
            @disable_init
            class MyClass:
                def __init__(self) -> None:
                    pass
        except TypeError as e:
            assert isinstance(e, TypeError)
            print(e)
        ```

    Example: Using a custom error message when the default constructor is called
        ```python
        from typing import Self

        class NoInitError(TypeError):
            def __init__(self) -> None:
                super().__init__("Please create instances of MyClass using MyClass.new()")

        @disable_init(error=NoInitError())
        class MyClass:
            @classmethod
            def new(cls) -> Self:
                return cls.__new__(cls)

        try:
            instance = MyClass()
        except NoInitError as e:
            assert str(e) == "Please create instances of MyClass using MyClass.new()"
            print(e)
        ```

    Args:
        cls: The class to be decorated.
        error: The error to raise if __init__ is called, if `None` a default
            [TypeError][] will be raised.

    Returns:
        A decorator that disables the `__init__` constructor of `cls`.
    """

    def decorator(inner_cls: TypeT) -> TypeT:
        return cast(
            TypeT,
            NoInitConstructibleMeta(
                inner_cls.__name__,
                inner_cls.__bases__,
                dict(inner_cls.__dict__),
                no_init_constructible_error=error,
            ),
        )

    if cls is None:
        return decorator
    return decorator(cls)