Hide keyboard shortcuts

Hot-keys 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

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 program is free software: you can redistribute it and/or modify 

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21from __future__ import annotations 

22 

23__all__ = ( 

24 "NamedKeyDict", 

25 "NamedKeyMapping", 

26 "NameLookupMapping", 

27 "NamedValueSet", 

28) 

29 

30from abc import abstractmethod 

31from typing import ( 

32 AbstractSet, 

33 Any, 

34 Dict, 

35 ItemsView, 

36 Iterable, 

37 Iterator, 

38 KeysView, 

39 Mapping, 

40 MutableMapping, 

41 MutableSet, 

42 TypeVar, 

43 Union, 

44 ValuesView, 

45) 

46from types import MappingProxyType 

47try: 

48 # If we're running mypy, we should have typing_extensions. 

49 # If we aren't running mypy, we shouldn't assume we do. 

50 # When we're safely on Python 3.8, we can import Protocol 

51 # from typing and avoid all of this. 

52 from typing_extensions import Protocol 

53 

54 class Named(Protocol): 

55 @property 

56 def name(self) -> str: 

57 pass 

58 

59except ImportError: 

60 Named = Any # type: ignore 

61 

62 

63K = TypeVar("K", bound=Named) 

64V = TypeVar("V") 

65 

66 

67class NamedKeyMapping(Mapping[K, V]): 

68 """An abstract base class for custom mappings whose keys are objects with 

69 a `str` ``name`` attribute, for which lookups on the name as well as the 

70 object are permitted. 

71 

72 Notes 

73 ----- 

74 In addition to the new `names` property and `byName` method, this class 

75 simply redefines the type signature for `__getitem__` and `get` that would 

76 otherwise be inherited from `Mapping`. That is only relevant for static 

77 type checking; the actual Python runtime doesn't care about types at all. 

78 """ 

79 

80 @property 

81 @abstractmethod 

82 def names(self) -> AbstractSet[str]: 

83 """The set of names associated with the keys, in the same order 

84 (`AbstractSet` [ `str` ]). 

85 """ 

86 raise NotImplementedError() 

87 

88 def byName(self) -> Dict[str, V]: 

89 """Return a `Mapping` with names as keys and the same values as 

90 ``self``. 

91 

92 Returns 

93 ------- 

94 dictionary : `dict` 

95 A dictionary with the same values (and iteration order) as 

96 ``self``, with `str` names as keys. This is always a new object, 

97 not a view. 

98 """ 

99 return dict(zip(self.names, self.values())) 

100 

101 @abstractmethod 

102 def __getitem__(self, key: Union[str, K]) -> V: 

103 raise NotImplementedError() 

104 

105 def get(self, key: Union[str, K], default: Any = None) -> Any: 

106 # Delegating to super is not allowed by typing, because it doesn't 

107 # accept str, but we know it just delegates to __getitem__, which does. 

108 return super().get(key, default) # type: ignore 

109 

110 

111NameLookupMapping = Union[NamedKeyMapping[K, V], Mapping[str, V]] 

112"""A type annotation alias for signatures that want to use ``mapping[s]`` 

113(or ``mapping.get(s)``) where ``s`` is a `str`, and don't care whether 

114``mapping.keys()`` returns a named objects or direct `str` instances. 

115""" 

116 

117 

118class NamedKeyMutableMapping(NamedKeyMapping[K, V], MutableMapping[K, V]): 

119 """An abstract base class that adds mutation to `NamedKeyMapping`. 

120 """ 

121 

122 @abstractmethod 

123 def __setitem__(self, key: Union[str, K], value: V) -> None: 

124 raise NotImplementedError() 

125 

126 @abstractmethod 

127 def __delitem__(self, key: Union[str, K]) -> None: 

128 raise NotImplementedError() 

129 

130 def pop(self, key: Union[str, K], default: Any = None) -> Any: 

131 # See comment in `NamedKeyMapping.get`; same logic applies here. 

132 return super().pop(key, default) # type: ignore 

133 

134 

135class NamedKeyDict(NamedKeyMutableMapping[K, V]): 

136 """A dictionary wrapper that require keys to have a ``.name`` attribute, 

137 and permits lookups using either key objects or their names. 

138 

139 Names can be used in place of keys when updating existing items, but not 

140 when adding new items. 

141 

142 It is assumed (but asserted) that all name equality is equivalent to key 

143 equality, either because the key objects define equality this way, or 

144 because different objects with the same name are never included in the same 

145 dictionary. 

146 

147 Parameters 

148 ---------- 

149 args 

150 All positional constructor arguments are forwarded directly to `dict`. 

151 Keyword arguments are not accepted, because plain strings are not valid 

152 keys for `NamedKeyDict`. 

153 

154 Raises 

155 ------ 

156 AttributeError 

157 Raised when an attempt is made to add an object with no ``.name`` 

158 attribute to the dictionary. 

159 AssertionError 

160 Raised when multiple keys have the same name. 

161 """ 

162 

163 __slots__ = ("_dict", "_names",) 

164 

165 def __init__(self, *args: Any): 

166 self._dict: Dict[K, V] = dict(*args) 

167 self._names = {key.name: key for key in self._dict} 

168 assert len(self._names) == len(self._dict), "Duplicate names in keys." 

169 

170 @property 

171 def names(self) -> KeysView[str]: 

172 """The set of names associated with the keys, in the same order 

173 (`~collections.abc.KeysView`). 

174 """ 

175 return self._names.keys() 

176 

177 def byName(self) -> Dict[str, V]: 

178 """Return a `dict` with names as keys and the same values as ``self``. 

179 """ 

180 return dict(zip(self._names.keys(), self._dict.values())) 

181 

182 def __len__(self) -> int: 

183 return len(self._dict) 

184 

185 def __iter__(self) -> Iterator[K]: 

186 return iter(self._dict) 

187 

188 def __str__(self) -> str: 

189 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items())) 

190 

191 def __repr__(self) -> str: 

192 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items())) 

193 

194 def __getitem__(self, key: Union[str, K]) -> V: 

195 if isinstance(key, str): 

196 return self._dict[self._names[key]] 

197 else: 

198 return self._dict[key] 

199 

200 def __setitem__(self, key: Union[str, K], value: V) -> None: 

201 if isinstance(key, str): 

202 self._dict[self._names[key]] = value 

203 else: 

204 assert self._names.get(key.name, key) == key, "Name is already associated with a different key." 

205 self._dict[key] = value 

206 self._names[key.name] = key 

207 

208 def __delitem__(self, key: Union[str, K]) -> None: 

209 if isinstance(key, str): 

210 del self._dict[self._names[key]] 

211 del self._names[key] 

212 else: 

213 del self._dict[key] 

214 del self._names[key.name] 

215 

216 def keys(self) -> KeysView[K]: 

217 return self._dict.keys() 

218 

219 def values(self) -> ValuesView[V]: 

220 return self._dict.values() 

221 

222 def items(self) -> ItemsView[K, V]: 

223 return self._dict.items() 

224 

225 def copy(self) -> NamedKeyDict[K, V]: 

226 result = NamedKeyDict.__new__(NamedKeyDict) 

227 result._dict = dict(self._dict) 

228 result._names = dict(self._names) 

229 return result 

230 

231 def freeze(self) -> None: 

232 """Disable all mutators, effectively transforming ``self`` into 

233 an immutable mapping. 

234 """ 

235 if not isinstance(self._dict, MappingProxyType): 

236 self._dict = MappingProxyType(self._dict) # type: ignore 

237 

238 

239class NamedValueSet(MutableSet[K]): 

240 """A custom mutable set class that requires elements to have a ``.name`` 

241 attribute, which can then be used as keys in `dict`-like lookup. 

242 

243 Names and elements can both be used with the ``in`` and ``del`` 

244 operators, `remove`, and `discard`. Names (but not elements) 

245 can be used with ``[]``-based element retrieval (not assignment) 

246 and the `get` method. `pop` can be used in either its `MutableSet` 

247 form (no arguments; an arbitrary element is returned) or its 

248 `MutableMapping` form (one or two arguments for the name and 

249 optional default value, respectively). 

250 

251 Parameters 

252 ---------- 

253 elements : `iterable` 

254 Iterable over elements to include in the set. 

255 

256 Raises 

257 ------ 

258 AttributeError 

259 Raised if one or more elements do not have a ``.name`` attribute. 

260 

261 Notes 

262 ----- 

263 Iteration order is guaranteed to be the same as insertion order (with 

264 the same general behavior as `dict` ordering). 

265 Like `dicts`, sets with the same elements will compare as equal even if 

266 their iterator order is not the same. 

267 """ 

268 

269 __slots__ = ("_dict",) 

270 

271 def __init__(self, elements: Iterable[K] = ()): 

272 self._dict = {element.name: element for element in elements} 

273 

274 @property 

275 def names(self) -> KeysView[str]: 

276 """The set of element names, in the same order 

277 (`~collections.abc.KeysView`). 

278 """ 

279 return self._dict.keys() 

280 

281 def asDict(self) -> Mapping[str, K]: 

282 """Return a mapping view with names as keys. 

283 

284 Returns 

285 ------- 

286 dict : `Mapping` 

287 A dictionary-like view with ``values() == self``. 

288 """ 

289 return self._dict 

290 

291 def __contains__(self, key: Any) -> bool: 

292 return getattr(key, "name", key) in self._dict 

293 

294 def __len__(self) -> int: 

295 return len(self._dict) 

296 

297 def __iter__(self) -> Iterator[K]: 

298 return iter(self._dict.values()) 

299 

300 def __str__(self) -> str: 

301 return "{{{}}}".format(", ".join(str(element) for element in self)) 

302 

303 def __repr__(self) -> str: 

304 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self)) 

305 

306 def __eq__(self, other: Any) -> Union[bool, NotImplemented]: 

307 if isinstance(other, NamedValueSet): 

308 return self._dict.keys() == other._dict.keys() 

309 else: 

310 return NotImplemented 

311 

312 def __hash__(self) -> int: 

313 return hash(frozenset(self._dict.keys())) 

314 

315 # As per Set's docs, overriding just __le__ and __ge__ for performance will 

316 # cover the other comparisons, too. 

317 

318 def __le__(self, other: AbstractSet[K]) -> Union[bool, NotImplemented]: 

319 if isinstance(other, NamedValueSet): 

320 return self._dict.keys() <= other._dict.keys() 

321 else: 

322 return NotImplemented 

323 

324 def __ge__(self, other: AbstractSet[K]) -> Union[bool, NotImplemented]: 

325 if isinstance(other, NamedValueSet): 

326 return self._dict.keys() >= other._dict.keys() 

327 else: 

328 return NotImplemented 

329 

330 def issubset(self, other: AbstractSet[K]) -> bool: 

331 return self <= other 

332 

333 def issuperset(self, other: AbstractSet[K]) -> bool: 

334 return self >= other 

335 

336 def __getitem__(self, name: str) -> K: 

337 return self._dict[name] 

338 

339 def get(self, name: str, default: Any = None) -> Any: 

340 """Return the element with the given name, or ``default`` if 

341 no such element is present. 

342 """ 

343 return self._dict.get(name, default) 

344 

345 def __delitem__(self, name: str) -> None: 

346 del self._dict[name] 

347 

348 def add(self, element: K) -> None: 

349 """Add an element to the set. 

350 

351 Raises 

352 ------ 

353 AttributeError 

354 Raised if the element does not have a ``.name`` attribute. 

355 """ 

356 self._dict[element.name] = element 

357 

358 def remove(self, element: Union[str, K]) -> Any: 

359 """Remove an element from the set. 

360 

361 Parameters 

362 ---------- 

363 element : `object` or `str` 

364 Element to remove or the string name thereof. Assumed to be an 

365 element if it has a ``.name`` attribute. 

366 

367 Raises 

368 ------ 

369 KeyError 

370 Raised if an element with the given name does not exist. 

371 """ 

372 del self._dict[getattr(element, "name", element)] 

373 

374 def discard(self, element: Union[str, K]) -> Any: 

375 """Remove an element from the set if it exists. 

376 

377 Does nothing if no matching element is present. 

378 

379 Parameters 

380 ---------- 

381 element : `object` or `str` 

382 Element to remove or the string name thereof. Assumed to be an 

383 element if it has a ``.name`` attribute. 

384 """ 

385 try: 

386 self.remove(element) 

387 except KeyError: 

388 pass 

389 

390 def pop(self, *args: str) -> K: 

391 """Remove and return an element from the set. 

392 

393 Parameters 

394 ---------- 

395 name : `str`, optional 

396 Name of the element to remove and return. Must be passed 

397 positionally. If not provided, an arbitrary element is 

398 removed and returned. 

399 

400 Raises 

401 ------ 

402 KeyError 

403 Raised if ``name`` is provided but ``default`` is not, and no 

404 matching element exists. 

405 """ 

406 if not args: 

407 return super().pop() 

408 else: 

409 return self._dict.pop(*args) 

410 

411 def copy(self) -> NamedValueSet[K]: 

412 result = NamedValueSet.__new__(NamedValueSet) 

413 result._dict = dict(self._dict) 

414 return result 

415 

416 def freeze(self) -> None: 

417 """Disable all mutators, effectively transforming ``self`` into 

418 an immutable set. 

419 """ 

420 if not isinstance(self._dict, MappingProxyType): 

421 self._dict = MappingProxyType(self._dict) # type: ignore