Coverage for python/lsst/utils/classes.py: 59%
38 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 21:58 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 21:58 -0800
1# This file is part of utils.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
11#
13"""Utilities to help with class creation.
14"""
16from __future__ import annotations
18__all__ = ["Singleton", "cached_getter", "immutable"]
20import functools
21from typing import Any, Callable, Dict, Type, TypeVar
24class Singleton(type):
25 """Metaclass to convert a class to a Singleton.
27 If this metaclass is used the constructor for the singleton class must
28 take no arguments. This is because a singleton class will only accept
29 the arguments the first time an instance is instantiated.
30 Therefore since you do not know if the constructor has been called yet it
31 is safer to always call it with no arguments and then call a method to
32 adjust state of the singleton.
33 """
35 _instances: Dict[Type, Any] = {}
37 # Signature is intentionally not substitutable for type.__call__ (no *args,
38 # **kwargs) to require classes that use this metaclass to have no
39 # constructor arguments.
40 def __call__(cls) -> Any: # type: ignore
41 if cls not in cls._instances:
42 cls._instances[cls] = super(Singleton, cls).__call__()
43 return cls._instances[cls]
46_T = TypeVar("_T", bound="Type")
49def immutable(cls: _T) -> _T:
50 """Decorate a class to simulate a simple form of immutability.
52 A class decorated as `immutable` may only set each of its attributes once;
53 any attempts to set an already-set attribute will raise `AttributeError`.
55 Notes
56 -----
57 Subclasses of classes marked with ``@immutable`` are also immutable.
59 Because this behavior interferes with the default implementation for the
60 ``pickle`` modules, `immutable` provides implementations of
61 ``__getstate__`` and ``__setstate__`` that override this behavior.
62 Immutable classes can then implement pickle via ``__reduce__`` or
63 ``__getnewargs__``.
65 Following the example of Python's built-in immutable types, such as `str`
66 and `tuple`, the `immutable` decorator provides a ``__copy__``
67 implementation that just returns ``self``, because there is no reason to
68 actually copy an object if none of its shared owners can modify it.
70 Similarly, objects that are recursively (i.e. are themselves immutable and
71 have only recursively immutable attributes) should also reimplement
72 ``__deepcopy__`` to return ``self``. This is not done by the decorator, as
73 it has no way of checking for recursive immutability.
74 """
76 def __setattr__(self: _T, name: str, value: Any) -> None: # noqa: N807
77 if hasattr(self, name):
78 raise AttributeError(f"{cls.__name__} instances are immutable.")
79 object.__setattr__(self, name, value)
81 # mypy says the variable here has signature (str, Any) i.e. no "self";
82 # I think it's just confused by descriptor stuff.
83 cls.__setattr__ = __setattr__ # type: ignore
85 def __getstate__(self: _T) -> dict: # noqa: N807
86 # Disable default state-setting when unpickled.
87 return {}
89 cls.__getstate__ = __getstate__
91 def __setstate__(self: _T, state: Any) -> None: # noqa: N807
92 # Disable default state-setting when copied.
93 # Sadly what works for pickle doesn't work for copy.
94 assert not state
96 cls.__setstate__ = __setstate__
98 def __copy__(self: _T) -> _T: # noqa: N807
99 return self
101 cls.__copy__ = __copy__
102 return cls
105_S = TypeVar("_S")
106_R = TypeVar("_R")
109def cached_getter(func: Callable[[_S], _R]) -> Callable[[_S], _R]:
110 """Decorate a method to cache the result.
112 Only works on methods that take only ``self``
113 as an argument, and returns the cached result on subsequent calls.
115 Notes
116 -----
117 This is intended primarily as a stopgap for Python 3.8's more sophisticated
118 ``functools.cached_property``, but it is also explicitly compatible with
119 the `immutable` decorator, which may not be true of ``cached_property``.
121 `cached_getter` guarantees that the cached value will be stored in
122 an attribute named ``_cached_{name-of-decorated-function}``. Classes that
123 use `cached_getter` are responsible for guaranteeing that this name is not
124 otherwise used, and is included if ``__slots__`` is defined.
125 """
126 attribute = f"_cached_{func.__name__}"
128 @functools.wraps(func)
129 def inner(self: _S) -> _R:
130 if not hasattr(self, attribute):
131 object.__setattr__(self, attribute, func(self))
132 return getattr(self, attribute)
134 return inner