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
« 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/>.
28from __future__ import annotations
30__all__ = ("DimensionRecordCache",)
32import copy
33from collections.abc import Callable, Iterator, Mapping
34from contextlib import contextmanager
36from ._record_set import DimensionRecordSet
37from ._universe import DimensionUniverse
40class DimensionRecordCache(Mapping[str, DimensionRecordSet]):
41 """A mapping of cached dimension records.
43 This object holds all records for elements where
44 `DimensionElement.is_cached` is `True`.
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`.
56 Notes
57 -----
58 The nested `DimensionRecordSet` objects should never be modified in place
59 except when returned by the `modifying` context manager.
60 """
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
68 def reset(self) -> None:
69 """Reset the cache, causing it to be fetched again on next use."""
70 self._records = None
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.
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.
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.
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
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.
133 Parameters
134 ----------
135 other : `DimensionRecordCache`
136 Other cache to potentially copy records from.
137 """
138 self._records = copy.deepcopy(other._records)
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
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]
153 def __iter__(self) -> Iterator[str]:
154 return iter(self._keys)
156 def __len__(self) -> int:
157 return len(self._keys)