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

27 "NamedValueMutableSet", 

28 "NamedValueSet", 

29 "NameLookupMapping", 

30 "NameMappingSetView", 

31) 

32 

33from abc import abstractmethod 

34from typing import ( 

35 AbstractSet, 

36 Any, 

37 Dict, 

38 ItemsView, 

39 Iterable, 

40 Iterator, 

41 KeysView, 

42 Mapping, 

43 MutableMapping, 

44 MutableSet, 

45 TypeVar, 

46 Union, 

47 ValuesView, 

48) 

49from types import MappingProxyType 

50try: 

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

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

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

54 # from typing and avoid all of this. 

55 from typing_extensions import Protocol 

56 

57 class Named(Protocol): 

58 @property 

59 def name(self) -> str: 

60 pass 

61 

62except ImportError: 

63 Named = Any # type: ignore 

64 

65 

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

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

68V = TypeVar("V") 

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

70 

71 

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

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

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

75 object are permitted. 

76 

77 Notes 

78 ----- 

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

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

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

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

83 """ 

84 

85 __slots__ = () 

86 

87 @property 

88 @abstractmethod 

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

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

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

92 """ 

93 raise NotImplementedError() 

94 

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

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

97 ``self``. 

98 

99 Returns 

100 ------- 

101 dictionary : `dict` 

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

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

104 not a view. 

105 """ 

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

107 

108 @abstractmethod 

109 def keys(self) -> NamedValueAbstractSet[K_co]: 

110 # TODO: docs 

111 raise NotImplementedError() 

112 

113 @abstractmethod 

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

115 raise NotImplementedError() 

116 

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

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

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

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

121 

122 

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

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

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

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

127""" 

128 

129 

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

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

132 """ 

133 

134 __slots__ = () 

135 

136 @abstractmethod 

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

138 raise NotImplementedError() 

139 

140 @abstractmethod 

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

142 raise NotImplementedError() 

143 

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

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

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

147 

148 

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

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

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

152 

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

154 when adding new items. 

155 

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

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

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

159 dictionary. 

160 

161 Parameters 

162 ---------- 

163 args 

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

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

166 keys for `NamedKeyDict`. 

167 

168 Raises 

169 ------ 

170 AttributeError 

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

172 attribute to the dictionary. 

173 AssertionError 

174 Raised when multiple keys have the same name. 

175 """ 

176 

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

178 

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

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

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

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

183 

184 @property 

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

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

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

188 """ 

189 return self._names.keys() 

190 

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

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

193 """ 

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

195 

196 def __len__(self) -> int: 

197 return len(self._dict) 

198 

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

200 return iter(self._dict) 

201 

202 def __str__(self) -> str: 

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

204 

205 def __repr__(self) -> str: 

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

207 

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

209 if isinstance(key, str): 

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

211 else: 

212 return self._dict[key] 

213 

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

215 if isinstance(key, str): 

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

217 else: 

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

219 self._dict[key] = value 

220 self._names[key.name] = key 

221 

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

223 if isinstance(key, str): 

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

225 del self._names[key] 

226 else: 

227 del self._dict[key] 

228 del self._names[key.name] 

229 

230 def keys(self) -> NamedValueAbstractSet[K]: 

231 return NameMappingSetView(self._names) 

232 

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

234 return self._dict.values() 

235 

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

237 return self._dict.items() 

238 

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

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

241 """ 

242 result = NamedKeyDict.__new__(NamedKeyDict) 

243 result._dict = dict(self._dict) 

244 result._names = dict(self._names) 

245 return result 

246 

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

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

249 an immutable mapping. 

250 

251 Returns 

252 ------- 

253 self : `NamedKeyMapping` 

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

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

256 to a new variable (and considering any previous references 

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

258 """ 

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

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

261 return self 

262 

263 

264class NamedValueAbstractSet(AbstractSet[K_co]): 

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

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

267 views to be supported. 

268 """ 

269 

270 __slots__ = () 

271 

272 @property 

273 @abstractmethod 

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

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

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

277 """ 

278 raise NotImplementedError() 

279 

280 @abstractmethod 

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

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 raise NotImplementedError() 

290 

291 @abstractmethod 

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

293 raise NotImplementedError() 

294 

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

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

297 no such element is present. 

298 """ 

299 try: 

300 return self[key] 

301 except KeyError: 

302 return default 

303 

304 @classmethod 

305 def _from_iterable(cls, iterable: Iterable[K_co]) -> NamedValueSet[K_co]: 

306 """Hook to ensure that inherited `collections.abc.Set` operators return 

307 `NamedValueSet` instances, not something else (see `collections.abc` 

308 documentation for more information). 

309 

310 Note that this behavior can only be guaranteed when both operands are 

311 `NamedValueAbstractSet` instances. 

312 """ 

313 return NamedValueSet(iterable) 

314 

315 

316class NameMappingSetView(NamedValueAbstractSet[K_co]): 

317 """A lightweight implementation of `NamedValueAbstractSet` backed by a 

318 mapping from name to named object. 

319 

320 Parameters 

321 ---------- 

322 mapping : `Mapping` [ `str`, `object` ] 

323 Mapping this object will provide a view of. 

324 """ 

325 def __init__(self, mapping: Mapping[str, K_co]): 

326 self._mapping = mapping 

327 

328 __slots__ = ("_mapping",) 

329 

330 @property 

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

332 # Docstring inherited from NamedValueAbstractSet. 

333 return self._mapping.keys() 

334 

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

336 # Docstring inherited from NamedValueAbstractSet. 

337 return self._mapping 

338 

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

340 if isinstance(key, str): 

341 return self._mapping[key] 

342 else: 

343 return self._mapping[key.name] 

344 

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

346 return getattr(key, "name", key) in self._mapping 

347 

348 def __len__(self) -> int: 

349 return len(self._mapping) 

350 

351 def __iter__(self) -> Iterator[K_co]: 

352 return iter(self._mapping.values()) 

353 

354 def __eq__(self, other: Any) -> bool: 

355 if isinstance(other, NamedValueAbstractSet): 

356 return self.names == other.names 

357 else: 

358 return set(self._mapping.values()) == other 

359 

360 def __le__(self, other: AbstractSet[K]) -> bool: 

361 if isinstance(other, NamedValueAbstractSet): 

362 return self.names <= other.names 

363 else: 

364 return set(self._mapping.values()) <= other 

365 

366 def __ge__(self, other: AbstractSet[K]) -> bool: 

367 if isinstance(other, NamedValueAbstractSet): 

368 return self.names >= other.names 

369 else: 

370 return set(self._mapping.values()) >= other 

371 

372 def __str__(self) -> str: 

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

374 

375 def __repr__(self) -> str: 

376 return f"NameMappingSetView({self._mapping})" 

377 

378 

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

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

381 `NamedValueAbstractSet`. 

382 

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

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

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

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

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

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

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

390 `NamedValueAbstractSet.__getitem__`). 

391 """ 

392 

393 __slots__ = () 

394 

395 @abstractmethod 

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

397 raise NotImplementedError() 

398 

399 @abstractmethod 

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

401 """Remove an element from the set. 

402 

403 Parameters 

404 ---------- 

405 element : `object` or `str` 

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

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

408 

409 Raises 

410 ------ 

411 KeyError 

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

413 """ 

414 raise NotImplementedError() 

415 

416 @abstractmethod 

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

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

419 

420 Does nothing if no matching element is present. 

421 

422 Parameters 

423 ---------- 

424 element : `object` or `str` 

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

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

427 """ 

428 raise NotImplementedError() 

429 

430 @abstractmethod 

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

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

433 

434 Parameters 

435 ---------- 

436 name : `str`, optional 

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

438 positionally. If not provided, an arbitrary element is 

439 removed and returned. 

440 

441 Raises 

442 ------ 

443 KeyError 

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

445 matching element exists. 

446 """ 

447 raise NotImplementedError() 

448 

449 

450class NamedValueSet(NameMappingSetView[K], NamedValueMutableSet[K]): 

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

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

453 

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

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

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

457 and the `get` method. 

458 

459 Parameters 

460 ---------- 

461 elements : `iterable` 

462 Iterable over elements to include in the set. 

463 

464 Raises 

465 ------ 

466 AttributeError 

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

468 

469 Notes 

470 ----- 

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

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

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

474 their iterator order is not the same. 

475 """ 

476 

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

478 super().__init__({element.name: element for element in elements}) 

479 

480 def __repr__(self) -> str: 

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

482 

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

484 return self <= other 

485 

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

487 return self >= other 

488 

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

490 del self._mapping[name] 

491 

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

493 """Add an element to the set. 

494 

495 Raises 

496 ------ 

497 AttributeError 

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

499 """ 

500 self._mapping[element.name] = element 

501 

502 def clear(self) -> None: 

503 # Docstring inherited. 

504 self._mapping.clear() 

505 

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

507 # Docstring inherited. 

508 del self._mapping[getattr(element, "name", element)] 

509 

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

511 # Docstring inherited. 

512 try: 

513 self.remove(element) 

514 except KeyError: 

515 pass 

516 

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

518 # Docstring inherited. 

519 if not args: 

520 return super().pop() 

521 else: 

522 return self._mapping.pop(*args) 

523 

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

525 """Add multiple new elements to the set. 

526 

527 Parameters 

528 ---------- 

529 elements : `Iterable` 

530 Elements to add. 

531 """ 

532 for element in elements: 

533 self.add(element) 

534 

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

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

537 """ 

538 result = NamedValueSet.__new__(NamedValueSet) 

539 result._mapping = dict(self._mapping) 

540 return result 

541 

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

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

544 an immutable set. 

545 

546 Returns 

547 ------- 

548 self : `NamedValueAbstractSet` 

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

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

551 to a new variable (and considering any previous references 

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

553 """ 

554 if not isinstance(self._mapping, MappingProxyType): 

555 self._mapping = MappingProxyType(self._mapping) # type: ignore 

556 return self 

557 

558 _mapping: Dict[str, K]