Coverage for python/lsst/daf/butler/dimensions/record_cache.py: 29%

46 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-16 10:44 +0000

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ("DimensionRecordCache",) 

31 

32import copy 

33from collections.abc import Callable, Iterator, Mapping 

34from contextlib import contextmanager 

35 

36from ._record_set import DimensionRecordSet 

37from ._universe import DimensionUniverse 

38 

39 

40class DimensionRecordCache(Mapping[str, DimensionRecordSet]): 

41 """A mapping of cached dimension records. 

42 

43 This object holds all records for elements where 

44 `DimensionElement.is_cached` is `True`. 

45 

46 Parameters 

47 ---------- 

48 universe : `DimensionUniverse` 

49 Definitions of all dimensions. 

50 fetch : `~collections.abc.Callable` 

51 A callable that takes no arguments and returns a `dict` mapping `str` 

52 element name to a `DimensionRecordSet` of all records for that element. 

53 They keys of the returned `dict` must be exactly the elements in 

54 ``universe`` for which `DimensionElement.is_cached` is `True`. 

55 

56 Notes 

57 ----- 

58 The nested `DimensionRecordSet` objects should never be modified in place 

59 except when returned by the `modifying` context manager. 

60 """ 

61 

62 def __init__(self, universe: DimensionUniverse, fetch: Callable[[], dict[str, DimensionRecordSet]]): 

63 self._universe = universe 

64 self._keys = [element.name for element in universe.elements if element.is_cached] 

65 self._records: dict[str, DimensionRecordSet] | None = None 

66 self._fetch = fetch 

67 

68 def reset(self) -> None: 

69 """Reset the cache, causing it to be fetched again on next use.""" 

70 self._records = None 

71 

72 @contextmanager 

73 def modifying(self, element: str) -> Iterator[DimensionRecordSet | None]: 

74 """Return a context manager for modifying the cache and database 

75 content consistently. 

76 

77 Parameters 

78 ---------- 

79 element : `str` 

80 Name of the dimension element whose records will be modified. 

81 If this is not a cached record, `None` will be returned and the 

82 context manager does nothing. 

83 

84 Returns 

85 ------- 

86 context : `contextlib.AbstractContextManager` [ `DimensionRecordSet` \ 

87 or `None` ] 

88 A context manager that when entered returns a `DimensionRecordSet` 

89 that should be modified in-place, or `None` if the cache is 

90 currently reset. 

91 

92 Notes 

93 ----- 

94 The returned context manager resets the cache when entered, and only 

95 restores the cache (along with the modifications to the returned 

96 `DimensionRecordSet`) if an exception is not raised during the context. 

97 It also takes care of updating any cache for "implied union" dimensions 

98 (e.g. ``band``, in the default dimension universe) when their targets 

99 are updated (e.g. ``physical_filter``). 

100 """ 

101 if element in self: 

102 records = self._records 

103 self._records = None 

104 if records is None: 

105 yield None 

106 else: 

107 yield records[element] 

108 for other_element_records in records.values(): 

109 other_element = other_element_records.element 

110 # If we've just updated the records of a dimension element 

111 # that is the implied union target of another (i.e. we've 

112 # updated physical_filter, and thus possibly updated the 

113 # set of band values). We need to update the cache for 

114 # the implied union target (i.e. band), too. 

115 if ( 

116 other_element.implied_union_target is not None 

117 and other_element.implied_union_target.name == element 

118 ): 

119 other_element_records.update( 

120 other_element.RecordClass( 

121 **{other_element.name: getattr(record, other_element.name)} 

122 ) 

123 for record in records[element] 

124 ) 

125 self._records = records 

126 else: 

127 yield None 

128 

129 def load_from(self, other: DimensionRecordCache) -> None: 

130 """Load records from another cache, but do nothing if it doesn't 

131 currently have any records. 

132 

133 Parameters 

134 ---------- 

135 other : `DimensionRecordCache` 

136 Other cache to potentially copy records from. 

137 """ 

138 self._records = copy.deepcopy(other._records) 

139 

140 def __contains__(self, key: object) -> bool: 

141 if not isinstance(key, str): 

142 return False 

143 if (element := self._universe.get(key)) is not None: 

144 return element.is_cached 

145 return False 

146 

147 def __getitem__(self, element: str) -> DimensionRecordSet: 

148 if self._records is None: 

149 self._records = self._fetch() 

150 assert self._records.keys() == set(self._keys), "Logic bug in fetch callback." 

151 return self._records[element] 

152 

153 def __iter__(self) -> Iterator[str]: 

154 return iter(self._keys) 

155 

156 def __len__(self) -> int: 

157 return len(self._keys)