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) 

64K_co = TypeVar("K_co", bound=Named, covariant=True) 

65V = TypeVar("V") 

66V_co = TypeVar("V_co", covariant=True) 

67 

68 

69class NamedKeyMapping(Mapping[K_co, V_co]): 

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

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

72 object are permitted. 

73 

74 Notes 

75 ----- 

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

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

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

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

80 """ 

81 

82 __slots__ = () 

83 

84 @property 

85 @abstractmethod 

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

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

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

89 """ 

90 raise NotImplementedError() 

91 

92 def byName(self) -> Dict[str, V_co]: 

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

94 ``self``. 

95 

96 Returns 

97 ------- 

98 dictionary : `dict` 

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

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

101 not a view. 

102 """ 

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

104 

105 @abstractmethod 

106 def __getitem__(self, key: Union[str, K_co]) -> V_co: 

107 raise NotImplementedError() 

108 

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

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

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

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

113 

114 

115NameLookupMapping = Union[NamedKeyMapping[K_co, V_co], Mapping[str, V_co]] 

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

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

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

119""" 

120 

121 

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

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

124 """ 

125 

126 __slots__ = () 

127 

128 @abstractmethod 

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

130 raise NotImplementedError() 

131 

132 @abstractmethod 

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

134 raise NotImplementedError() 

135 

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

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

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

139 

140 

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

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

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

144 

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

146 when adding new items. 

147 

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

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

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

151 dictionary. 

152 

153 Parameters 

154 ---------- 

155 args 

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

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

158 keys for `NamedKeyDict`. 

159 

160 Raises 

161 ------ 

162 AttributeError 

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

164 attribute to the dictionary. 

165 AssertionError 

166 Raised when multiple keys have the same name. 

167 """ 

168 

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

170 

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

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

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

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

175 

176 @property 

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

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

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

180 """ 

181 return self._names.keys() 

182 

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

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

185 """ 

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

187 

188 def __len__(self) -> int: 

189 return len(self._dict) 

190 

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

192 return iter(self._dict) 

193 

194 def __str__(self) -> str: 

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

196 

197 def __repr__(self) -> str: 

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

199 

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

201 if isinstance(key, str): 

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

203 else: 

204 return self._dict[key] 

205 

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

207 if isinstance(key, str): 

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

209 else: 

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

211 self._dict[key] = value 

212 self._names[key.name] = key 

213 

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

215 if isinstance(key, str): 

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

217 del self._names[key] 

218 else: 

219 del self._dict[key] 

220 del self._names[key.name] 

221 

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

223 return self._dict.keys() 

224 

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

226 return self._dict.values() 

227 

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

229 return self._dict.items() 

230 

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

232 """Return a new `NamedKeyDict` with the same elements. 

233 """ 

234 result = NamedKeyDict.__new__(NamedKeyDict) 

235 result._dict = dict(self._dict) 

236 result._names = dict(self._names) 

237 return result 

238 

239 def freeze(self) -> NamedKeyMapping[K, V]: 

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

241 an immutable mapping. 

242 

243 Returns 

244 ------- 

245 self : `NamedKeyMapping` 

246 While ``self`` is modified in-place, it is also returned with a 

247 type anotation that reflects its new, frozen state; assigning it 

248 to a new variable (and considering any previous references 

249 invalidated) should allow for more accurate static type checking. 

250 """ 

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

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

253 return self 

254 

255 

256class NamedValueAbstractSet(AbstractSet[K_co]): 

257 """An abstract base class for custom sets whose elements are objects with 

258 a `str` ``name`` attribute, allowing some dict-like operations and 

259 views to be supported. 

260 """ 

261 

262 __slots__ = () 

263 

264 @property 

265 @abstractmethod 

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

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

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

269 """ 

270 raise NotImplementedError() 

271 

272 @abstractmethod 

273 def asMapping(self) -> Mapping[str, K_co]: 

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

275 

276 Returns 

277 ------- 

278 dict : `Mapping` 

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

280 """ 

281 raise NotImplementedError() 

282 

283 @abstractmethod 

284 def __getitem__(self, key: Union[str, K_co]) -> K_co: 

285 raise NotImplementedError() 

286 

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

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

289 no such element is present. 

290 """ 

291 try: 

292 return self[key] 

293 except KeyError: 

294 return default 

295 

296 

297class NamedValueMutableSet(NamedValueAbstractSet[K], MutableSet[K]): 

298 """An abstract base class that adds mutation interfaces to 

299 `NamedValueAbstractSet`. 

300 

301 Methods that can add new elements to the set are unchanged from their 

302 `MutableSet` definitions, while those that only remove them can generally 

303 accept names or element instances. `pop` can be used in either its 

304 `MutableSet` form (no arguments; an arbitrary element is returned) or its 

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

306 default value, respectively). A `MutableMapping`-like `__delitem__` 

307 interface is also included, which takes only names (like 

308 `NamedValueAbstractSet.__getitem__`). 

309 """ 

310 

311 __slots__ = () 

312 

313 @abstractmethod 

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

315 raise NotImplementedError() 

316 

317 @abstractmethod 

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

319 """Remove an element from the set. 

320 

321 Parameters 

322 ---------- 

323 element : `object` or `str` 

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

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

326 

327 Raises 

328 ------ 

329 KeyError 

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

331 """ 

332 raise NotImplementedError() 

333 

334 @abstractmethod 

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

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

337 

338 Does nothing if no matching element is present. 

339 

340 Parameters 

341 ---------- 

342 element : `object` or `str` 

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

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

345 """ 

346 raise NotImplementedError() 

347 

348 @abstractmethod 

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

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

351 

352 Parameters 

353 ---------- 

354 name : `str`, optional 

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

356 positionally. If not provided, an arbitrary element is 

357 removed and returned. 

358 

359 Raises 

360 ------ 

361 KeyError 

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

363 matching element exists. 

364 """ 

365 raise NotImplementedError() 

366 

367 

368class NamedValueSet(NamedValueMutableSet[K]): 

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

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

371 

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

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

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

375 and the `get` method. 

376 

377 Parameters 

378 ---------- 

379 elements : `iterable` 

380 Iterable over elements to include in the set. 

381 

382 Raises 

383 ------ 

384 AttributeError 

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

386 

387 Notes 

388 ----- 

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

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

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

392 their iterator order is not the same. 

393 """ 

394 

395 __slots__ = ("_dict",) 

396 

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

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

399 

400 @property 

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

402 # Docstring inherited. 

403 return self._dict.keys() 

404 

405 def asMapping(self) -> Mapping[str, K]: 

406 # Docstring inherited. 

407 return self._dict 

408 

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

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

411 

412 def __len__(self) -> int: 

413 return len(self._dict) 

414 

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

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

417 

418 def __str__(self) -> str: 

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

420 

421 def __repr__(self) -> str: 

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

423 

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

425 if isinstance(other, NamedValueSet): 

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

427 else: 

428 return NotImplemented 

429 

430 def __hash__(self) -> int: 

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

432 

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

434 # cover the other comparisons, too. 

435 

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

437 if isinstance(other, NamedValueSet): 

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

439 else: 

440 return NotImplemented 

441 

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

443 if isinstance(other, NamedValueSet): 

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

445 else: 

446 return NotImplemented 

447 

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

449 return self <= other 

450 

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

452 return self >= other 

453 

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

455 if isinstance(key, str): 

456 return self._dict[key] 

457 else: 

458 return self._dict[key.name] 

459 

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

461 # Docstring inherited 

462 if isinstance(key, str): 

463 return self._dict.get(key, default) 

464 else: 

465 return self._dict.get(key.name, default) 

466 

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

468 del self._dict[name] 

469 

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

471 """Add an element to the set. 

472 

473 Raises 

474 ------ 

475 AttributeError 

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

477 """ 

478 self._dict[element.name] = element 

479 

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

481 # Docstring inherited. 

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

483 

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

485 # Docstring inherited. 

486 try: 

487 self.remove(element) 

488 except KeyError: 

489 pass 

490 

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

492 # Docstring inherited. 

493 if not args: 

494 return super().pop() 

495 else: 

496 return self._dict.pop(*args) 

497 

498 def update(self, elements: Iterable[K]) -> None: 

499 """Add multple new elements to the set. 

500 

501 Parameters 

502 ---------- 

503 elements : `Iterable` 

504 Elements to add. 

505 """ 

506 for element in elements: 

507 self.add(element) 

508 

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

510 """Return a new `NamedValueSet` with the same elements. 

511 """ 

512 result = NamedValueSet.__new__(NamedValueSet) 

513 result._dict = dict(self._dict) 

514 return result 

515 

516 def freeze(self) -> NamedValueAbstractSet[K]: 

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

518 an immutable set. 

519 

520 Returns 

521 ------- 

522 self : `NamedValueAbstractSet` 

523 While ``self`` is modified in-place, it is also returned with a 

524 type anotation that reflects its new, frozen state; assigning it 

525 to a new variable (and considering any previous references 

526 invalidated) should allow for more accurate static type checking. 

527 """ 

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

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

530 return self