Skip to content

Commit e7161b8

Browse files
committed
typing: Improve documentation
Improve the docs to point out that disabling the `__init__` for existing classes via a sub-class is complicated, so it is only recommended to use both the decorator and metaclass on classes inheriting from `object` directly. Also improve the documentation of `NoInitConstructibleMeta` to be more complete and give an example on using with `abc.ABCMeta`. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 7a0051c commit e7161b8

File tree

1 file changed

+104
-5
lines changed

1 file changed

+104
-5
lines changed

src/frequenz/core/typing.py

+104-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33

44
"""Type hints and utility functions for type checking and types.
55
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]).
914
"""
1015

1116
from collections.abc import Callable
@@ -48,7 +53,14 @@ def disable_init(
4853
Warning:
4954
This decorator will use a custom metaclass to disable the `__init__` constructor
5055
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.
5264
5365
Example: Basic example defining a class with a factory method
5466
To be able to type hint the class correctly, you can declare the instance
@@ -135,7 +147,94 @@ def decorator(inner_cls: TypeT) -> TypeT:
135147

136148

137149
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+
"""
139238

140239
# We need to use noqa here because pydoclint can't figure out that
141240
# _get_no_init_constructible_error() returns a TypeError.

0 commit comments

Comments
 (0)