Coverage for python/lsst/utils/classes.py: 59%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

38 statements  

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# 

12 

13"""Utilities to help with class creation. 

14""" 

15 

16from __future__ import annotations 

17 

18__all__ = ["Singleton", "cached_getter", "immutable"] 

19 

20from typing import ( 

21 Any, 

22 Callable, 

23 Dict, 

24 Type, 

25 TypeVar, 

26) 

27import functools 

28 

29 

30class Singleton(type): 

31 """Metaclass to convert a class to a Singleton. 

32 

33 If this metaclass is used the constructor for the singleton class must 

34 take no arguments. This is because a singleton class will only accept 

35 the arguments the first time an instance is instantiated. 

36 Therefore since you do not know if the constructor has been called yet it 

37 is safer to always call it with no arguments and then call a method to 

38 adjust state of the singleton. 

39 """ 

40 

41 _instances: Dict[Type, Any] = {} 

42 

43 # Signature is intentionally not substitutable for type.__call__ (no *args, 

44 # **kwargs) to require classes that use this metaclass to have no 

45 # constructor arguments. 

46 def __call__(cls) -> Any: # type: ignore 

47 if cls not in cls._instances: 

48 cls._instances[cls] = super(Singleton, cls).__call__() 

49 return cls._instances[cls] 

50 

51 

52_T = TypeVar("_T", bound="Type") 

53 

54 

55def immutable(cls: _T) -> _T: 

56 """Decorate a class to simulate a simple form of immutability. 

57 

58 A class decorated as `immutable` may only set each of its attributes once; 

59 any attempts to set an already-set attribute will raise `AttributeError`. 

60 

61 Notes 

62 ----- 

63 Subclasses of classes marked with ``@immutable`` are also immutable. 

64 

65 Because this behavior interferes with the default implementation for the 

66 ``pickle`` modules, `immutable` provides implementations of 

67 ``__getstate__`` and ``__setstate__`` that override this behavior. 

68 Immutable classes can then implement pickle via ``__reduce__`` or 

69 ``__getnewargs__``. 

70 

71 Following the example of Python's built-in immutable types, such as `str` 

72 and `tuple`, the `immutable` decorator provides a ``__copy__`` 

73 implementation that just returns ``self``, because there is no reason to 

74 actually copy an object if none of its shared owners can modify it. 

75 

76 Similarly, objects that are recursively (i.e. are themselves immutable and 

77 have only recursively immutable attributes) should also reimplement 

78 ``__deepcopy__`` to return ``self``. This is not done by the decorator, as 

79 it has no way of checking for recursive immutability. 

80 """ 

81 def __setattr__(self: _T, name: str, value: Any) -> None: # noqa: N807 

82 if hasattr(self, name): 

83 raise AttributeError(f"{cls.__name__} instances are immutable.") 

84 object.__setattr__(self, name, value) 

85 # mypy says the variable here has signature (str, Any) i.e. no "self"; 

86 # I think it's just confused by descriptor stuff. 

87 cls.__setattr__ = __setattr__ # type: ignore 

88 

89 def __getstate__(self: _T) -> dict: # noqa: N807 

90 # Disable default state-setting when unpickled. 

91 return {} 

92 cls.__getstate__ = __getstate__ 

93 

94 def __setstate__(self: _T, state: Any) -> None: # noqa: N807 

95 # Disable default state-setting when copied. 

96 # Sadly what works for pickle doesn't work for copy. 

97 assert not state 

98 cls.__setstate__ = __setstate__ 

99 

100 def __copy__(self: _T) -> _T: # noqa: N807 

101 return self 

102 cls.__copy__ = __copy__ 

103 return cls 

104 

105 

106_S = TypeVar("_S") 

107_R = TypeVar("_R") 

108 

109 

110def cached_getter(func: Callable[[_S], _R]) -> Callable[[_S], _R]: 

111 """Decorate a method to cache the result. 

112 

113 Only works on methods that take only ``self`` 

114 as an argument, and returns the cached result on subsequent calls. 

115 

116 Notes 

117 ----- 

118 This is intended primarily as a stopgap for Python 3.8's more sophisticated 

119 ``functools.cached_property``, but it is also explicitly compatible with 

120 the `immutable` decorator, which may not be true of ``cached_property``. 

121 

122 `cached_getter` guarantees that the cached value will be stored in 

123 an attribute named ``_cached_{name-of-decorated-function}``. Classes that 

124 use `cached_getter` are responsible for guaranteeing that this name is not 

125 otherwise used, and is included if ``__slots__`` is defined. 

126 """ 

127 attribute = f"_cached_{func.__name__}" 

128 

129 @functools.wraps(func) 

130 def inner(self: _S) -> _R: 

131 if not hasattr(self, attribute): 

132 object.__setattr__(self, attribute, func(self)) 

133 return getattr(self, attribute) 

134 

135 return inner