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 "IndexedTupleDict", 

25 "NamedKeyDict", 

26 "NamedKeyMapping", 

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 Tuple, 

43 TypeVar, 

44 Union, 

45 ValuesView, 

46) 

47from types import MappingProxyType 

48try: 

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

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

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

52 # from typing and avoid all of this. 

53 from typing_extensions import Protocol 

54 

55 class Named(Protocol): 

56 @property 

57 def name(self) -> str: 

58 pass 

59 

60except ImportError: 

61 Named = Any # type: ignore 

62 

63 

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

65V = TypeVar("V") 

66 

67 

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

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

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

71 object are permitted. 

72 

73 Notes 

74 ----- 

75 In addition to the new `names` property and ``byName` attribute, this class 

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

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

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

79 """ 

80 

81 @property 

82 @abstractmethod 

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

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

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

86 """ 

87 raise NotImplementedError() 

88 

89 @abstractmethod 

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

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

92 ``self``. 

93 

94 Returns 

95 ------- 

96 dictionary : `dict` 

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

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

99 not a view. 

100 """ 

101 raise NotImplementedError() 

102 

103 @abstractmethod 

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

105 raise NotImplementedError() 

106 

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

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

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

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

111 

112 

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

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

115 """ 

116 

117 @abstractmethod 

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

119 raise NotImplementedError() 

120 

121 @abstractmethod 

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

123 raise NotImplementedError() 

124 

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

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

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

128 

129 

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

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

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

133 

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

135 when adding new items. 

136 

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

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

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

140 dictionary. 

141 

142 Parameters 

143 ---------- 

144 args 

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

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

147 keys for `NamedKeyDict`. 

148 

149 Raises 

150 ------ 

151 AttributeError 

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

153 attribute to the dictionary. 

154 AssertionError 

155 Raised when multiple keys have the same name. 

156 """ 

157 

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

159 

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

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

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

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

164 

165 @property 

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

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

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

169 """ 

170 return self._names.keys() 

171 

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

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

174 """ 

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

176 

177 def __len__(self) -> int: 

178 return len(self._dict) 

179 

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

181 return iter(self._dict) 

182 

183 def __str__(self) -> str: 

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

185 

186 def __repr__(self) -> str: 

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

188 

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

190 if isinstance(key, str): 

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

192 else: 

193 return self._dict[key] 

194 

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

196 if isinstance(key, str): 

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

198 else: 

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

200 self._dict[key] = value 

201 self._names[key.name] = key 

202 

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

204 if isinstance(key, str): 

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

206 del self._names[key] 

207 else: 

208 del self._dict[key] 

209 del self._names[key.name] 

210 

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

212 return self._dict.keys() 

213 

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

215 return self._dict.values() 

216 

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

218 return self._dict.items() 

219 

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

221 result = NamedKeyDict.__new__(NamedKeyDict) 

222 result._dict = dict(self._dict) 

223 result._names = dict(self._names) 

224 return result 

225 

226 def freeze(self) -> None: 

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

228 an immutable mapping. 

229 """ 

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

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

232 

233 

234class NamedValueSet(MutableSet[K]): 

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

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

237 

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

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

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

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

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

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

244 optional default value, respectively). 

245 

246 Parameters 

247 ---------- 

248 elements : `iterable` 

249 Iterable over elements to include in the set. 

250 

251 Raises 

252 ------ 

253 AttributeError 

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

255 

256 Notes 

257 ----- 

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

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

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

261 their iterator order is not the same. 

262 """ 

263 

264 __slots__ = ("_dict",) 

265 

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

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

268 

269 @property 

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

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

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

273 """ 

274 return self._dict.keys() 

275 

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

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

278 

279 Returns 

280 ------- 

281 dict : `Mapping` 

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

283 """ 

284 return self._dict 

285 

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

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

288 

289 def __len__(self) -> int: 

290 return len(self._dict) 

291 

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

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

294 

295 def __str__(self) -> str: 

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

297 

298 def __repr__(self) -> str: 

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

300 

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

302 if isinstance(other, NamedValueSet): 

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

304 else: 

305 return NotImplemented 

306 

307 def __hash__(self) -> int: 

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

309 

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

311 # cover the other comparisons, too. 

312 

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

314 if isinstance(other, NamedValueSet): 

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

316 else: 

317 return NotImplemented 

318 

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

320 if isinstance(other, NamedValueSet): 

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

322 else: 

323 return NotImplemented 

324 

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

326 return self <= other 

327 

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

329 return self >= other 

330 

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

332 return self._dict[name] 

333 

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

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

336 no such element is present. 

337 """ 

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

339 

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

341 del self._dict[name] 

342 

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

344 """Add an element to the set. 

345 

346 Raises 

347 ------ 

348 AttributeError 

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

350 """ 

351 self._dict[element.name] = element 

352 

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

354 """Remove an element from the set. 

355 

356 Parameters 

357 ---------- 

358 element : `object` or `str` 

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

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

361 

362 Raises 

363 ------ 

364 KeyError 

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

366 """ 

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

368 

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

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

371 

372 Does nothing if no matching element is present. 

373 

374 Parameters 

375 ---------- 

376 element : `object` or `str` 

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

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

379 """ 

380 try: 

381 self.remove(element) 

382 except KeyError: 

383 pass 

384 

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

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

387 

388 Parameters 

389 ---------- 

390 name : `str`, optional 

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

392 positionally. If not provided, an arbitrary element is 

393 removed and returned. 

394 

395 Raises 

396 ------ 

397 KeyError 

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

399 matching element exists. 

400 """ 

401 if not args: 

402 return super().pop() 

403 else: 

404 return self._dict.pop(*args) 

405 

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

407 result = NamedValueSet.__new__(NamedValueSet) 

408 result._dict = dict(self._dict) 

409 return result 

410 

411 def freeze(self) -> None: 

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

413 an immutable set. 

414 """ 

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

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

417 

418 

419class IndexedTupleDict(NamedKeyMapping[K, V]): 

420 """An immutable mapping that combines a tuple of values with a (possibly 

421 shared) mapping from key to tuple index. 

422 

423 Parameters 

424 ---------- 

425 indices: `NamedKeyDict` 

426 Mapping from key to integer index in the values tuple. This mapping 

427 is used as-is, not copied or converted to a true `dict`, which means 

428 that the caller must guarantee that it will not be modified by other 

429 (shared) owners in the future. 

430 The caller is also responsible for guaranteeing that the indices in 

431 the mapping are all valid for the given tuple. 

432 values: `tuple` 

433 Tuple of values for the dictionary. This may have a length greater 

434 than the length of indices; these values are not considered part of 

435 the mapping. 

436 """ 

437 

438 __slots__ = ("_indices", "_values") 

439 

440 def __init__(self, indices: NamedKeyDict[K, int], values: Tuple[V, ...]): 

441 assert tuple(indices.values()) == tuple(range(len(values))) 

442 self._indices = indices 

443 self._values = values 

444 

445 @property 

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

447 return self._indices.names 

448 

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

450 return dict(zip(self.names, self._values)) 

451 

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

453 return self._values[self._indices[key]] 

454 

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

456 return iter(self._indices) 

457 

458 def __len__(self) -> int: 

459 return len(self._indices) 

460 

461 def __str__(self) -> str: 

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

463 

464 def __repr__(self) -> str: 

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

466 

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

468 return key in self._indices 

469 

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

471 return self._indices.keys() 

472 

473 # Tuple meets all requirements of ValuesView, but the Python typing system 

474 # doesn't recognize it as substitutable, perhaps because it only really is 

475 # for immutable mappings where there's no need to worry about the view 

476 # being updated because the mapping changed. 

477 def values(self) -> Tuple[V, ...]: # type: ignore 

478 return self._values 

479 

480 # Let Mapping base class provide items(); we can't do it any more 

481 # efficiently ourselves. 

482 

483 # These private attributes need to have types annotated outside __new__ 

484 # because mypy hasn't learned (yet) how to infer instance attribute types 

485 # there they way it can with __init__. 

486 _indices: NamedKeyDict[K, int] 

487 _values: Tuple[V, ...]