Skip to content

Classes

toolbox_python.classes 🔗

Summary

The classes module is designed for functions to be executed on classes; not within classes. For any methods/functions that should be added to classes, you should consider re-designing the original class, or sub-classing it to make further alterations.

get_full_class_name 🔗

get_full_class_name(obj: Any) -> str

Summary

This function is designed to extract the full name of a class, including the name of the module from which it was loaded.

Details

Note, this is designed to retrieve the underlying class name of an object, not the instance name of an object. This is useful for debugging purposes, or for logging.

Parameters:

Name Type Description Default
obj Any

The object for which you want to retrieve the full name.

required

Returns:

Type Description
str

The full name of the class of the object.

Examples
Set up
1
>>> from toolbox_python.classes import get_full_class_name

Example 1: Check the name of a standard class
1
>>> print(get_full_class_name(str))
Terminal
str

Conclusion: Successful class name extraction.

Example 2: Check the name of an imported class
1
2
>>> from random import Random
>>> print(get_full_class_name(Random))
Terminal
random.Random

Conclusion: Successful class name extraction.

Credit

Full credit goes to:
https://stackoverflow.com/questions/18176602/how-to-get-the-name-of-an-exception-that-was-caught-in-python#answer-58045927

Source code in src/toolbox_python/classes.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def get_full_class_name(obj: Any) -> str:
    """
    !!! note "Summary"
        This function is designed to extract the full name of a class, including the name of the module from which it was loaded.

    ???+ abstract "Details"
        Note, this is designed to retrieve the underlying _class name_ of an object, not the _instance name_ of an object. This is useful for debugging purposes, or for logging.

    Params:
        obj (Any):
            The object for which you want to retrieve the full name.

    Returns:
        (str):
            The full name of the class of the object.

    ???+ example "Examples"

        ```{.py .python linenums="1" title="Set up"}
        >>> from toolbox_python.classes import get_full_class_name
        ```

        ```{.py .python linenums="1" title="Example 1: Check the name of a standard class"}
        >>> print(get_full_class_name(str))
        ```
        <div class="result" markdown>
        ```{.sh .shell title="Terminal"}
        str
        ```
        !!! success "Conclusion: Successful class name extraction."
        </div>

        ```{.py .python linenums="1" title="Example 2: Check the name of an imported class"}
        >>> from random import Random
        >>> print(get_full_class_name(Random))
        ```
        <div class="result" markdown>
        ```{.sh .shell title="Terminal"}
        random.Random
        ```
        !!! success "Conclusion: Successful class name extraction."
        </div>

    ??? success "Credit"
        Full credit goes to:<br>
        https://stackoverflow.com/questions/18176602/how-to-get-the-name-of-an-exception-that-was-caught-in-python#answer-58045927
    """
    module: str = obj.__class__.__module__
    if module is None or module == str.__class__.__module__:
        return obj.__class__.__name__
    return module + "." + obj.__class__.__name__

class_property 🔗

Bases: property

Summary

Similar to property, but allows class-level properties. That is, a property whose getter is like a classmethod.

Details

The wrapped method may explicitly use the classmethod decorator (which must become before this decorator), or the classmethod may be omitted (it is implicit through use of this decorator).

Parameters:

Name Type Description Default
fget callable

The function that computes the value of this property (in particular, the function when this is used as a decorator) a la property.

required
doc str

The docstring for the property--by default inherited from the getter function.

None
Examples

Normal usage is as a decorator:

Example 1: Normal usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> class Foo:
...     _bar_internal = 1
...
...     @class_property
...     def bar(cls):
...         return cls._bar_internal + 1
>>>
>>> print(f"Class attribute: `{Foo.bar}`")
>>>
>>> foo_instance = Foo()
>>> print(f"Instantiated class: `{foo_instance.bar}`")
>>>
>>> foo_instance._bar_internal = 2
>>> print(f"Modified instance attribute: `{foo_instance.bar}`")  # Ignores instance attributes
Terminal
Class attribute: `2`
Instantiated class: `2`
Modified instance attribute: `2`
Note that in the third print() statement, the instance attribute _bar_internal is ignored. This is because class_property is designed to be used as a class-level property, not an instance-level property. See the Notes section for more details.

Conclusion: Successful usage.

As previously noted, a class_property is limited to implementing read-only attributes:

Example 2: Read-only attributes
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> class Foo:
...     _bar_internal = 1
...
...     @class_property
...     def bar(cls):
...         return cls._bar_internal
...
...     @bar.setter
...     def bar(cls, value):
...         cls._bar_internal = value
Terminal
Traceback (most recent call last):
...
NotImplementedError: class_property can only be read-only; use a metaclass to implement modifiable class-level properties

Conclusion: Failed to set a class property.

Notes
  • @class_property only works for read-only properties. It does not currently allow writeable/deletable properties, due to subtleties of how Python descriptors work. In order to implement such properties on a class, a metaclass for that class must be implemented.
  • @class_property is not a drop-in replacement for property. It is designed to be used as a class-level property, not an instance-level property. If you need to use it as an instance-level property, you will need to use the @property decorator instead.
  • @class_property is defined at class scope, not instance scope. This means that it is not bound to the instance of the class, but rather to the class itself; hence the name class_property. This means that it is designed to be used as a class-level property and is accessed through the class itself, not an instance-level property which is accessed through the instance of a class. If it is necessary to access the instance-level property, you will need to use the instance itself (eg. instantiated_class_name._internal_attribute) or create an instance-level property using the @property decorator.
Credit

This @class_property object is heavily inspired by the astropy library. All credit goes to them. See: details.

Source code in src/toolbox_python/classes.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
class class_property(property):
    """
    !!! note "Summary"
        Similar to `property`, but allows class-level properties. That is, a property whose getter is like a `classmethod`.

    ???+ abstract "Details"
        The wrapped method may explicitly use the `classmethod` decorator (which must become before this decorator), or the `classmethod` may be omitted (it is implicit through use of this decorator).

    Params:
        fget (callable):
            The function that computes the value of this property (in particular, the function when this is used as a decorator) a la `property`.

        doc (str, optional):
            The docstring for the property--by default inherited from the getter function.

    ???+ example "Examples"

        Normal usage is as a decorator:

        ```{.py .python linenums="1" title="Example 1: Normal usage"}
        >>> class Foo:
        ...     _bar_internal = 1
        ...
        ...     @class_property
        ...     def bar(cls):
        ...         return cls._bar_internal + 1
        >>>
        >>> print(f"Class attribute: `{Foo.bar}`")
        >>>
        >>> foo_instance = Foo()
        >>> print(f"Instantiated class: `{foo_instance.bar}`")
        >>>
        >>> foo_instance._bar_internal = 2
        >>> print(f"Modified instance attribute: `{foo_instance.bar}`")  # Ignores instance attributes
        ```
        <div class="result" markdown>
        ```{.sh .shell title="Terminal"}
        Class attribute: `2`
        Instantiated class: `2`
        Modified instance attribute: `2`
        ```
        Note that in the third `print()` statement, the instance attribute `_bar_internal` is ignored. This is because `class_property` is designed to be used as a class-level property, not an instance-level property. See the Notes section for more details.
        !!! success "Conclusion: Successful usage."
        </div>

        As previously noted, a `class_property` is limited to implementing read-only attributes:

        ```{.py .python linenums="1" title="Example 2: Read-only attributes"}
        >>> class Foo:
        ...     _bar_internal = 1
        ...
        ...     @class_property
        ...     def bar(cls):
        ...         return cls._bar_internal
        ...
        ...     @bar.setter
        ...     def bar(cls, value):
        ...         cls._bar_internal = value
        ```
        <div class="result" markdown>
        ```{.sh .shell title="Terminal"}
        Traceback (most recent call last):
        ...
        NotImplementedError: class_property can only be read-only; use a metaclass to implement modifiable class-level properties
        ```
        !!! failure "Conclusion: Failed to set a class property."
        </div>

    ???+ info "Notes"
        - `@class_property` only works for *read-only* properties. It does not currently allow writeable/deletable properties, due to subtleties of how Python descriptors work. In order to implement such properties on a class, a metaclass for that class must be implemented.
        - `@class_property` is not a drop-in replacement for `property`. It is designed to be used as a class-level property, not an instance-level property. If you need to use it as an instance-level property, you will need to use the `@property` decorator instead.
        - `@class_property` is defined at class scope, not instance scope. This means that it is not bound to the instance of the class, but rather to the class itself; hence the name `class_property`. This means that it is designed to be used as a class-level property and is accessed through the class itself, not an instance-level property which is accessed through the instance of a class. If it is necessary to access the instance-level property, you will need to use the instance itself (eg. `instantiated_class_name._internal_attribute`) or create an instance-level property using the `@property` decorator.

    ???+ success "Credit"
        This `@class_property` object is heavily inspired by the [`astropy`](https://www.astropy.org/) library. All credit goes to them. See: [details](https://github.com/astropy/astropy/blob/e4993fffb54e19b04bc4e9af084984650bc0a46f/astropy/utils/decorators.py#L551-L722).
    """

    def __new__(cls, fget=None, doc=None):

        if fget is None:

            # Being used as a decorator-return a wrapper that implements decorator syntax
            def wrapper(func):
                return cls(func, doc=doc)

            return wrapper

        return super().__new__(cls)

    def __init__(self, fget, doc=None):

        fget = self._wrap_fget(fget)
        super().__init__(fget=fget, doc=doc)

        # There is a bug in Python where `self.__doc__` doesn't get set properly on instances of property subclasses if the `doc` argument was used rather than taking the docstring from `fget`.
        # Related Python issue: https://bugs.python.org/issue24766
        if doc is not None:
            self.__doc__ = doc

    def __get__(self, obj, objtype) -> Any:  # type: ignore[override]
        # The base `property.__get__` will just return self here; instead we pass `objtype` through to the original wrapped function (which takes the `class` as its sole argument). This is how we obtain the `class` attribute and not the `instance` attribute, and the key difference between `@property` and `@class_property`. This is also the same as `classmethod.__get__()`, but we need to pass `objtype` rather than `obj` to the wrapped function.
        val: Any = self.fget.__wrapped__(objtype)  # type: ignore[union-attr]
        return val

    @staticmethod
    def _wrap_fget(orig_fget):
        if isinstance(orig_fget, classmethod):
            orig_fget = orig_fget.__func__

        # Using standard `functools.wraps` for simplicity.
        @wraps(orig_fget)  # pragma: no cover
        def fget(obj):
            return orig_fget(obj.__class__)

        return fget

    # def getter(self, fget) -> property:
    #     return super().getter(self._wrap_fget(fget))

    def setter(self, fset) -> Any:
        raise NotImplementedError(
            "class_property can only be read-only; use a metaclass to implement modifiable class-level properties"
        )

    def deleter(self, fdel) -> Any:
        raise NotImplementedError(
            "class_property can only be read-only; use a metaclass to implement modifiable class-level properties"
        )
__new__ 🔗
__new__(fget=None, doc=None)
Source code in src/toolbox_python/classes.py
200
201
202
203
204
205
206
207
208
209
210
def __new__(cls, fget=None, doc=None):

    if fget is None:

        # Being used as a decorator-return a wrapper that implements decorator syntax
        def wrapper(func):
            return cls(func, doc=doc)

        return wrapper

    return super().__new__(cls)
__init__ 🔗
__init__(fget, doc=None)
Source code in src/toolbox_python/classes.py
212
213
214
215
216
217
218
219
220
def __init__(self, fget, doc=None):

    fget = self._wrap_fget(fget)
    super().__init__(fget=fget, doc=doc)

    # There is a bug in Python where `self.__doc__` doesn't get set properly on instances of property subclasses if the `doc` argument was used rather than taking the docstring from `fget`.
    # Related Python issue: https://bugs.python.org/issue24766
    if doc is not None:
        self.__doc__ = doc
__doc__ instance-attribute 🔗
__doc__ = doc
__get__ 🔗
__get__(obj, objtype) -> Any
Source code in src/toolbox_python/classes.py
222
223
224
225
def __get__(self, obj, objtype) -> Any:  # type: ignore[override]
    # The base `property.__get__` will just return self here; instead we pass `objtype` through to the original wrapped function (which takes the `class` as its sole argument). This is how we obtain the `class` attribute and not the `instance` attribute, and the key difference between `@property` and `@class_property`. This is also the same as `classmethod.__get__()`, but we need to pass `objtype` rather than `obj` to the wrapped function.
    val: Any = self.fget.__wrapped__(objtype)  # type: ignore[union-attr]
    return val
setter 🔗
setter(fset) -> Any
Source code in src/toolbox_python/classes.py
242
243
244
245
def setter(self, fset) -> Any:
    raise NotImplementedError(
        "class_property can only be read-only; use a metaclass to implement modifiable class-level properties"
    )
deleter 🔗
deleter(fdel) -> Any
Source code in src/toolbox_python/classes.py
247
248
249
250
def deleter(self, fdel) -> Any:
    raise NotImplementedError(
        "class_property can only be read-only; use a metaclass to implement modifiable class-level properties"
    )