|
3 | 3 |
|
4 | 4 | """Type hints and utility functions for type checking and types.
|
5 | 5 |
|
6 |
| -For now this module only provides a decorator to disable the `__init__` constructor of |
7 |
| -a class, to force the use of a factory method to create instances. See |
8 |
| -[disable_init][frequenz.core.typing.disable_init] for more information. |
| 6 | +This module provides a decorator to disable the `__init__` constructor of a class, to |
| 7 | +force the use of a factory method to create instances. See |
| 8 | +[`@disable_init`][frequenz.core.typing.disable_init] for more information. |
| 9 | +
|
| 10 | +It also provides a metaclass used by the decorator to disable the `__init__`: |
| 11 | +[`NoInitConstructibleMeta`][frequenz.core.typing.NoInitConstructibleMeta]. This is |
| 12 | +useful mostly for disabling `__init__` while having to use another metaclass too (like |
| 13 | +[`abc.ABCMeta`][abc.ABCMeta]). |
9 | 14 | """
|
10 | 15 |
|
11 | 16 | from collections.abc import Callable
|
@@ -48,7 +53,14 @@ def disable_init(
|
48 | 53 | Warning:
|
49 | 54 | This decorator will use a custom metaclass to disable the `__init__` constructor
|
50 | 55 | of the class, so if your class already uses a custom metaclass, you should be
|
51 |
| - aware of potential conflicts. |
| 56 | + aware of potential conflicts. See |
| 57 | + [`NoInitConstructibleMeta`][frequenz.core.typing.NoInitConstructibleMeta] for an |
| 58 | + example on how to use more than one metaclass. |
| 59 | +
|
| 60 | + It is also recommended to apply this decorator only to classes inheriting from |
| 61 | + `object` directly (i.e. not explicitly inheriting from any other classes), as |
| 62 | + things can also get tricky when applying the constructor to a sub-class for the |
| 63 | + first time. |
52 | 64 |
|
53 | 65 | Example: Basic example defining a class with a factory method
|
54 | 66 | To be able to type hint the class correctly, you can declare the instance
|
@@ -135,7 +147,94 @@ def decorator(inner_cls: TypeT) -> TypeT:
|
135 | 147 |
|
136 | 148 |
|
137 | 149 | class NoInitConstructibleMeta(type):
|
138 |
| - """A metaclass that disables the __init__ constructor.""" |
| 150 | + """A metaclass that disables the `__init__` constructor. |
| 151 | +
|
| 152 | + This metaclass can be used to disable the `__init__` constructor of a class. It is |
| 153 | + intended to be used with classes that don't provide a default constructor and |
| 154 | + require the use of a factory method to create instances. |
| 155 | +
|
| 156 | + When marking a class using this metaclass, the class cannot be even declared with a |
| 157 | + `__init__` method, as it will raise a `TypeError` when the class is created, as soon |
| 158 | + as the class is parsed by the Python interpreter. It will also raise a `TypeError` |
| 159 | + when the `__init__` method is called. |
| 160 | +
|
| 161 | + To create an instance you must provide a factory method, using `__new__`. |
| 162 | +
|
| 163 | + Warning: |
| 164 | + It is also recommended to apply this metaclass only to classes inheriting from |
| 165 | + `object` directly (i.e. not explicitly inheriting from any other classes), as |
| 166 | + things can also get tricky when applying the constructor to a sub-class for the |
| 167 | + first time. |
| 168 | +
|
| 169 | + Example: Basic example defining a class with a factory method |
| 170 | + To be able to type hint the class correctly, you can declare the instance |
| 171 | + attributes in the class body, and then use a factory method to create instances. |
| 172 | +
|
| 173 | + ```python |
| 174 | + from typing import Self |
| 175 | +
|
| 176 | + class MyClass(meta=NoInitConstructibleMeta): |
| 177 | + value: int |
| 178 | +
|
| 179 | + @classmethod |
| 180 | + def new(cls, value: int = 1) -> Self: |
| 181 | + self = cls.__new__(cls) |
| 182 | + self.value = value |
| 183 | + return self |
| 184 | +
|
| 185 | + instance = MyClass.new() |
| 186 | +
|
| 187 | + # Calling the default constructor (__init__) will raise a TypeError |
| 188 | + try: |
| 189 | + instance = MyClass() |
| 190 | + except TypeError as e: |
| 191 | + print(e) |
| 192 | + ``` |
| 193 | +
|
| 194 | + Hint: |
| 195 | + The [`@disable_init`][frequenz.core.typing.disable_init] decorator is a more |
| 196 | + convenient way to use this metaclass. |
| 197 | +
|
| 198 | + Example: Example combining with other metaclass |
| 199 | + A typical case where you might want this is to combine with |
| 200 | + [`abc.ABCMeta`][abc.ABCMeta] to create an abstract class that doesn't provide a |
| 201 | + default constructor. |
| 202 | +
|
| 203 | + ```python |
| 204 | + from abc import ABCMeta, abstractmethod |
| 205 | + from typing import Self |
| 206 | +
|
| 207 | + class NoInitConstructibleABCMeta(ABCMeta, NoInitConstructibleMeta): |
| 208 | + pass |
| 209 | +
|
| 210 | + class MyAbstractClass(meta=NoInitConstructibleABCMeta): |
| 211 | + @abstractmethod |
| 212 | + def do_something(self) -> None: |
| 213 | + ... |
| 214 | +
|
| 215 | + class MyClass(MyAbstractClass): |
| 216 | + value: int |
| 217 | +
|
| 218 | + @classmethod |
| 219 | + def new(cls, value: int = 1) -> Self: |
| 220 | + self = cls.__new__(cls) |
| 221 | + self.value = value |
| 222 | + return self |
| 223 | +
|
| 224 | + def do_something(self) -> None: |
| 225 | + print("Doing something") |
| 226 | +
|
| 227 | + instance = MyClass.new() |
| 228 | + instance.do_something() |
| 229 | +
|
| 230 | + # Calling the default constructor (__init__) will raise a TypeError |
| 231 | + try: |
| 232 | + instance = MyClass() |
| 233 | + except TypeError as e: |
| 234 | + print(e) |
| 235 | + ``` |
| 236 | +
|
| 237 | + """ |
139 | 238 |
|
140 | 239 | # We need to use noqa here because pydoclint can't figure out that
|
141 | 240 | # _get_no_init_constructible_error() returns a TypeError.
|
|
0 commit comments