Coverage for python/lsst/daf/butler/dimensions/_data_coordinate_iterable.py: 37%

205 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-27 09: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/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ( 

31 "DataCoordinateIterable", 

32 "DataCoordinateSet", 

33 "DataCoordinateSequence", 

34) 

35 

36from abc import abstractmethod 

37from collections.abc import Collection, Iterable, Iterator, Sequence, Set 

38from typing import Any, overload 

39 

40from ._coordinate import DataCoordinate 

41from ._graph import DimensionGraph 

42from ._universe import DimensionUniverse 

43 

44 

45class DataCoordinateIterable(Iterable[DataCoordinate]): 

46 """An abstract base class for homogeneous iterables of data IDs. 

47 

48 All elements of a `DataCoordinateIterable` identify the same set of 

49 dimensions (given by the `graph` property) and generally have the same 

50 `DataCoordinate.hasFull` and `DataCoordinate.hasRecords` flag values. 

51 """ 

52 

53 __slots__ = () 

54 

55 @staticmethod 

56 def fromScalar(dataId: DataCoordinate) -> _ScalarDataCoordinateIterable: 

57 """Return a `DataCoordinateIterable` containing the single data ID. 

58 

59 Parameters 

60 ---------- 

61 dataId : `DataCoordinate` 

62 Data ID to adapt. Must be a true `DataCoordinate` instance, not 

63 an arbitrary mapping. No runtime checking is performed. 

64 

65 Returns 

66 ------- 

67 iterable : `DataCoordinateIterable` 

68 A `DataCoordinateIterable` instance of unspecified (i.e. 

69 implementation-detail) subclass. Guaranteed to implement 

70 the `collections.abc.Sized` (i.e. `__len__`) and 

71 `collections.abc.Container` (i.e. `__contains__`) interfaces as 

72 well as that of `DataCoordinateIterable`. 

73 """ 

74 return _ScalarDataCoordinateIterable(dataId) 

75 

76 @property 

77 @abstractmethod 

78 def graph(self) -> DimensionGraph: 

79 """Dimensions identified by these data IDs (`DimensionGraph`).""" 

80 raise NotImplementedError() 

81 

82 @property 

83 def universe(self) -> DimensionUniverse: 

84 """Universe that defines all known compatible dimensions. 

85 

86 (`DimensionUniverse`). 

87 """ 

88 return self.graph.universe 

89 

90 @abstractmethod 

91 def hasFull(self) -> bool: 

92 """Indicate if all data IDs in this iterable identify all dimensions. 

93 

94 Not just required dimensions. 

95 

96 Returns 

97 ------- 

98 state : `bool` 

99 If `True`, ``all(d.hasFull() for d in iterable)`` is guaranteed. 

100 If `False`, no guarantees are made. 

101 """ 

102 raise NotImplementedError() 

103 

104 @abstractmethod 

105 def hasRecords(self) -> bool: 

106 """Return whether all data IDs in this iterable contain records. 

107 

108 Returns 

109 ------- 

110 state : `bool` 

111 If `True`, ``all(d.hasRecords() for d in iterable)`` is guaranteed. 

112 If `False`, no guarantees are made. 

113 """ 

114 raise NotImplementedError() 

115 

116 def toSet(self) -> DataCoordinateSet: 

117 """Transform this iterable into a `DataCoordinateSet`. 

118 

119 Returns 

120 ------- 

121 set : `DataCoordinateSet` 

122 A `DatasetCoordinateSet` instance with the same elements as 

123 ``self``, after removing any duplicates. May be ``self`` if it is 

124 already a `DataCoordinateSet`. 

125 """ 

126 return DataCoordinateSet( 

127 frozenset(self), 

128 graph=self.graph, 

129 hasFull=self.hasFull(), 

130 hasRecords=self.hasRecords(), 

131 check=False, 

132 ) 

133 

134 def toSequence(self) -> DataCoordinateSequence: 

135 """Transform this iterable into a `DataCoordinateSequence`. 

136 

137 Returns 

138 ------- 

139 seq : `DataCoordinateSequence` 

140 A new `DatasetCoordinateSequence` with the same elements as 

141 ``self``, in the same order. May be ``self`` if it is already a 

142 `DataCoordinateSequence`. 

143 """ 

144 return DataCoordinateSequence( 

145 tuple(self), graph=self.graph, hasFull=self.hasFull(), hasRecords=self.hasRecords(), check=False 

146 ) 

147 

148 @abstractmethod 

149 def subset(self, graph: DimensionGraph) -> DataCoordinateIterable: 

150 """Return a subset iterable. 

151 

152 This subset iterable returns data IDs that identify a subset of the 

153 dimensions that this one's do. 

154 

155 Parameters 

156 ---------- 

157 graph : `DimensionGraph` 

158 Dimensions to be identified by the data IDs in the returned 

159 iterable. Must be a subset of ``self.graph``. 

160 

161 Returns 

162 ------- 

163 iterable : `DataCoordinateIterable` 

164 A `DataCoordinateIterable` with ``iterable.graph == graph``. 

165 May be ``self`` if ``graph == self.graph``. Elements are 

166 equivalent to those that would be created by calling 

167 `DataCoordinate.subset` on all elements in ``self``, possibly 

168 with deduplication and/or reordering (depending on the subclass, 

169 which may make more specific guarantees). 

170 """ 

171 raise NotImplementedError() 

172 

173 

174class _ScalarDataCoordinateIterable(DataCoordinateIterable): 

175 """An iterable for a single `DataCoordinate`. 

176 

177 A `DataCoordinateIterable` implementation that adapts a single 

178 `DataCoordinate` instance. 

179 

180 This class should only be used directly by other code in the module in 

181 which it is defined; all other code should interact with it only through 

182 the `DataCoordinateIterable` interface. 

183 

184 Parameters 

185 ---------- 

186 dataId : `DataCoordinate` 

187 The data ID to adapt. 

188 """ 

189 

190 def __init__(self, dataId: DataCoordinate): 

191 self._dataId = dataId 

192 

193 __slots__ = ("_dataId",) 

194 

195 def __iter__(self) -> Iterator[DataCoordinate]: 

196 yield self._dataId 

197 

198 def __len__(self) -> int: 

199 return 1 

200 

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

202 if isinstance(key, DataCoordinate): 

203 return key == self._dataId 

204 else: 

205 return False 

206 

207 @property 

208 def graph(self) -> DimensionGraph: 

209 # Docstring inherited from DataCoordinateIterable. 

210 return self._dataId.graph 

211 

212 def hasFull(self) -> bool: 

213 # Docstring inherited from DataCoordinateIterable. 

214 return self._dataId.hasFull() 

215 

216 def hasRecords(self) -> bool: 

217 # Docstring inherited from DataCoordinateIterable. 

218 return self._dataId.hasRecords() 

219 

220 def subset(self, graph: DimensionGraph) -> _ScalarDataCoordinateIterable: 

221 # Docstring inherited from DataCoordinateIterable. 

222 return _ScalarDataCoordinateIterable(self._dataId.subset(graph)) 

223 

224 

225class _DataCoordinateCollectionBase(DataCoordinateIterable): 

226 """A partial iterable implementation backed by native Python collection. 

227 

228 A partial `DataCoordinateIterable` implementation that is backed by a 

229 native Python collection. 

230 

231 This class is intended only to be used as an intermediate base class for 

232 `DataCoordinateIterables` that assume a more specific type of collection 

233 and can hence make more informed choices for how to implement some methods. 

234 

235 Parameters 

236 ---------- 

237 dataIds : `collections.abc.Collection` [ `DataCoordinate` ] 

238 A collection of `DataCoordinate` instances, with dimensions equal to 

239 ``graph``. 

240 graph : `DimensionGraph` 

241 Dimensions identified by all data IDs in the set. 

242 hasFull : `bool`, optional 

243 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

244 `True` for all given data IDs. If `False`, no such guarantee is made, 

245 and `hasFull` will always return `False`. If `None` (default), 

246 `hasFull` will be computed from the given data IDs, immediately if 

247 ``check`` is `True`, or on first use if ``check`` is `False`. 

248 hasRecords : `bool`, optional 

249 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

250 returns `True` for all given data IDs. If `False`, no such guarantee 

251 is made and `hasRecords` will always return `False`. If `None` 

252 (default), `hasRecords` will be computed from the given data IDs, 

253 immediately if ``check`` is `True`, or on first use if ``check`` is 

254 `False`. 

255 check: `bool`, optional 

256 If `True` (default) check that all data IDs are consistent with the 

257 given ``graph`` and state flags at construction. If `False`, no 

258 checking will occur. 

259 """ 

260 

261 def __init__( 

262 self, 

263 dataIds: Collection[DataCoordinate], 

264 graph: DimensionGraph, 

265 *, 

266 hasFull: bool | None = None, 

267 hasRecords: bool | None = None, 

268 check: bool = True, 

269 ): 

270 self._dataIds = dataIds 

271 self._graph = graph 

272 if check: 

273 for dataId in self._dataIds: 

274 if hasFull and not dataId.hasFull(): 

275 raise ValueError(f"{dataId} is not complete, but is required to be.") 

276 if hasRecords and not dataId.hasRecords(): 

277 raise ValueError(f"{dataId} has no records, but is required to.") 

278 if dataId.graph != self._graph: 

279 raise ValueError(f"Bad dimensions {dataId.graph}; expected {self._graph}.") 

280 if hasFull is None: 

281 hasFull = all(dataId.hasFull() for dataId in self._dataIds) 

282 if hasRecords is None: 

283 hasRecords = all(dataId.hasRecords() for dataId in self._dataIds) 

284 self._hasFull = hasFull 

285 self._hasRecords = hasRecords 

286 

287 __slots__ = ("_graph", "_dataIds", "_hasFull", "_hasRecords") 

288 

289 @property 

290 def graph(self) -> DimensionGraph: 

291 # Docstring inherited from DataCoordinateIterable. 

292 return self._graph 

293 

294 def hasFull(self) -> bool: 

295 # Docstring inherited from DataCoordinateIterable. 

296 if self._hasFull is None: 

297 self._hasFull = all(dataId.hasFull() for dataId in self._dataIds) 

298 return self._hasFull 

299 

300 def hasRecords(self) -> bool: 

301 # Docstring inherited from DataCoordinateIterable. 

302 if self._hasRecords is None: 

303 self._hasRecords = all(dataId.hasRecords() for dataId in self._dataIds) 

304 return self._hasRecords 

305 

306 def toSet(self) -> DataCoordinateSet: 

307 # Docstring inherited from DataCoordinateIterable. 

308 # Override base class to pass in attributes instead of results of 

309 # method calls for _hasFull and _hasRecords - those can be None, 

310 # and hence defer checking if that's what the user originally wanted. 

311 return DataCoordinateSet( 

312 frozenset(self._dataIds), 

313 graph=self._graph, 

314 hasFull=self._hasFull, 

315 hasRecords=self._hasRecords, 

316 check=False, 

317 ) 

318 

319 def toSequence(self) -> DataCoordinateSequence: 

320 # Docstring inherited from DataCoordinateIterable. 

321 # Override base class to pass in attributes instead of results of 

322 # method calls for _hasFull and _hasRecords - those can be None, 

323 # and hence defer checking if that's what the user originally wanted. 

324 return DataCoordinateSequence( 

325 tuple(self._dataIds), 

326 graph=self._graph, 

327 hasFull=self._hasFull, 

328 hasRecords=self._hasRecords, 

329 check=False, 

330 ) 

331 

332 def __iter__(self) -> Iterator[DataCoordinate]: 

333 return iter(self._dataIds) 

334 

335 def __len__(self) -> int: 

336 return len(self._dataIds) 

337 

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

339 key = DataCoordinate.standardize(key, universe=self.universe) 

340 return key in self._dataIds 

341 

342 def _subsetKwargs(self, graph: DimensionGraph) -> dict[str, Any]: 

343 """Return constructor kwargs useful for subclasses implementing subset. 

344 

345 Parameters 

346 ---------- 

347 graph : `DimensionGraph` 

348 Dimensions passed to `subset`. 

349 

350 Returns 

351 ------- 

352 **kwargs 

353 A dict with `hasFull`, `hasRecords`, and `check` keys, associated 

354 with the appropriate values for a `subset` operation with the given 

355 dimensions. 

356 """ 

357 hasFull: bool | None 

358 if graph.dimensions <= self.graph.required: 

359 hasFull = True 

360 else: 

361 hasFull = self._hasFull 

362 return dict(hasFull=hasFull, hasRecords=self._hasRecords, check=False) 

363 

364 

365class DataCoordinateSet(_DataCoordinateCollectionBase): 

366 """Iterable iteration that is set-like. 

367 

368 A `DataCoordinateIterable` implementation that adds some set-like 

369 functionality, and is backed by a true set-like object. 

370 

371 Parameters 

372 ---------- 

373 dataIds : `collections.abc.Set` [ `DataCoordinate` ] 

374 A set of `DataCoordinate` instances, with dimensions equal to 

375 ``graph``. If this is a mutable object, the caller must be able to 

376 guarantee that it will not be modified by any other holders. 

377 graph : `DimensionGraph` 

378 Dimensions identified by all data IDs in the set. 

379 hasFull : `bool`, optional 

380 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

381 `True` for all given data IDs. If `False`, no such guarantee is made, 

382 and `DataCoordinateSet.hasFull` will always return `False`. If `None` 

383 (default), `DataCoordinateSet.hasFull` will be computed from the given 

384 data IDs, immediately if ``check`` is `True`, or on first use if 

385 ``check`` is `False`. 

386 hasRecords : `bool`, optional 

387 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

388 returns `True` for all given data IDs. If `False`, no such guarantee 

389 is made and `DataCoordinateSet.hasRecords` will always return `False`. 

390 If `None` (default), `DataCoordinateSet.hasRecords` will be computed 

391 from the given data IDs, immediately if ``check`` is `True`, or on 

392 first use if ``check`` is `False`. 

393 check: `bool`, optional 

394 If `True` (default) check that all data IDs are consistent with the 

395 given ``graph`` and state flags at construction. If `False`, no 

396 checking will occur. 

397 

398 Notes 

399 ----- 

400 `DataCoordinateSet` does not formally implement the `collections.abc.Set` 

401 interface, because that requires many binary operations to accept any 

402 set-like object as the other argument (regardless of what its elements 

403 might be), and it's much easier to ensure those operations never behave 

404 surprisingly if we restrict them to `DataCoordinateSet` or (sometimes) 

405 `DataCoordinateIterable`, and in most cases restrict that they identify 

406 the same dimensions. In particular: 

407 

408 - a `DataCoordinateSet` will compare as not equal to any object that is 

409 not a `DataCoordinateSet`, even native Python sets containing the exact 

410 same elements; 

411 

412 - subset/superset comparison _operators_ (``<``, ``>``, ``<=``, ``>=``) 

413 require both operands to be `DataCoordinateSet` instances that have the 

414 same dimensions (i.e. ``graph`` attribute); 

415 

416 - `issubset`, `issuperset`, and `isdisjoint` require the other argument to 

417 be a `DataCoordinateIterable` with the same dimensions; 

418 

419 - operators that create new sets (``&``, ``|``, ``^``, ``-``) require both 

420 operands to be `DataCoordinateSet` instances that have the same 

421 dimensions _and_ the same ``dtype``; 

422 

423 - named methods that create new sets (`intersection`, `union`, 

424 `symmetric_difference`, `difference`) require the other operand to be a 

425 `DataCoordinateIterable` with the same dimensions _and_ the same 

426 ``dtype``. 

427 

428 In addition, when the two operands differ in the return values of `hasFull` 

429 and/or `hasRecords`, we make no guarantees about what those methods will 

430 return on the new `DataCoordinateSet` (other than that they will accurately 

431 reflect what elements are in the new set - we just don't control which 

432 elements are contributed by each operand). 

433 """ 

434 

435 def __init__( 

436 self, 

437 dataIds: Set[DataCoordinate], 

438 graph: DimensionGraph, 

439 *, 

440 hasFull: bool | None = None, 

441 hasRecords: bool | None = None, 

442 check: bool = True, 

443 ): 

444 super().__init__(dataIds, graph, hasFull=hasFull, hasRecords=hasRecords, check=check) 

445 

446 _dataIds: Set[DataCoordinate] 

447 

448 __slots__ = () 

449 

450 def __str__(self) -> str: 

451 return str(set(self._dataIds)) 

452 

453 def __repr__(self) -> str: 

454 return ( 

455 f"DataCoordinateSet({set(self._dataIds)}, {self._graph!r}, " 

456 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})" 

457 ) 

458 

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

460 if isinstance(other, DataCoordinateSet): 

461 return self._graph == other._graph and self._dataIds == other._dataIds 

462 return False 

463 

464 def __le__(self, other: DataCoordinateSet) -> bool: 

465 if self.graph != other.graph: 

466 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

467 return self._dataIds <= other._dataIds 

468 

469 def __ge__(self, other: DataCoordinateSet) -> bool: 

470 if self.graph != other.graph: 

471 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

472 return self._dataIds >= other._dataIds 

473 

474 def __lt__(self, other: DataCoordinateSet) -> bool: 

475 if self.graph != other.graph: 

476 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

477 return self._dataIds < other._dataIds 

478 

479 def __gt__(self, other: DataCoordinateSet) -> bool: 

480 if self.graph != other.graph: 

481 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

482 return self._dataIds > other._dataIds 

483 

484 def issubset(self, other: DataCoordinateIterable) -> bool: 

485 """Test whether ``self`` contains all data IDs in ``other``. 

486 

487 Parameters 

488 ---------- 

489 other : `DataCoordinateIterable` 

490 An iterable of data IDs with ``other.graph == self.graph``. 

491 

492 Returns 

493 ------- 

494 issubset : `bool` 

495 `True` if all data IDs in ``self`` are also in ``other``, and 

496 `False` otherwise. 

497 """ 

498 if self.graph != other.graph: 

499 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

500 return self._dataIds <= other.toSet()._dataIds 

501 

502 def issuperset(self, other: DataCoordinateIterable) -> bool: 

503 """Test whether ``other`` contains all data IDs in ``self``. 

504 

505 Parameters 

506 ---------- 

507 other : `DataCoordinateIterable` 

508 An iterable of data IDs with ``other.graph == self.graph``. 

509 

510 Returns 

511 ------- 

512 issuperset : `bool` 

513 `True` if all data IDs in ``other`` are also in ``self``, and 

514 `False` otherwise. 

515 """ 

516 if self.graph != other.graph: 

517 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

518 return self._dataIds >= other.toSet()._dataIds 

519 

520 def isdisjoint(self, other: DataCoordinateIterable) -> bool: 

521 """Test whether there are no data IDs in both ``self`` and ``other``. 

522 

523 Parameters 

524 ---------- 

525 other : `DataCoordinateIterable` 

526 An iterable of data IDs with ``other.graph == self.graph``. 

527 

528 Returns 

529 ------- 

530 isdisjoint : `bool` 

531 `True` if there are no data IDs in both ``self`` and ``other``, and 

532 `False` otherwise. 

533 """ 

534 if self.graph != other.graph: 

535 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

536 return self._dataIds.isdisjoint(other.toSet()._dataIds) 

537 

538 def __and__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

539 if self.graph != other.graph: 

540 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

541 return DataCoordinateSet(self._dataIds & other._dataIds, self.graph, check=False) 

542 

543 def __or__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

544 if self.graph != other.graph: 

545 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

546 return DataCoordinateSet(self._dataIds | other._dataIds, self.graph, check=False) 

547 

548 def __xor__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

549 if self.graph != other.graph: 

550 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

551 return DataCoordinateSet(self._dataIds ^ other._dataIds, self.graph, check=False) 

552 

553 def __sub__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

554 if self.graph != other.graph: 

555 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

556 return DataCoordinateSet(self._dataIds - other._dataIds, self.graph, check=False) 

557 

558 def intersection(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

559 """Return a new set that contains all data IDs from parameters. 

560 

561 Parameters 

562 ---------- 

563 other : `DataCoordinateIterable` 

564 An iterable of data IDs with ``other.graph == self.graph``. 

565 

566 Returns 

567 ------- 

568 intersection : `DataCoordinateSet` 

569 A new `DataCoordinateSet` instance. 

570 """ 

571 if self.graph != other.graph: 

572 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

573 return DataCoordinateSet(self._dataIds & other.toSet()._dataIds, self.graph, check=False) 

574 

575 def union(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

576 """Return a new set that contains all data IDs in either parameters. 

577 

578 Parameters 

579 ---------- 

580 other : `DataCoordinateIterable` 

581 An iterable of data IDs with ``other.graph == self.graph``. 

582 

583 Returns 

584 ------- 

585 intersection : `DataCoordinateSet` 

586 A new `DataCoordinateSet` instance. 

587 """ 

588 if self.graph != other.graph: 

589 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

590 return DataCoordinateSet(self._dataIds | other.toSet()._dataIds, self.graph, check=False) 

591 

592 def symmetric_difference(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

593 """Return a new set with all data IDs in either parameters, not both. 

594 

595 Parameters 

596 ---------- 

597 other : `DataCoordinateIterable` 

598 An iterable of data IDs with ``other.graph == self.graph``. 

599 

600 Returns 

601 ------- 

602 intersection : `DataCoordinateSet` 

603 A new `DataCoordinateSet` instance. 

604 """ 

605 if self.graph != other.graph: 

606 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

607 return DataCoordinateSet(self._dataIds ^ other.toSet()._dataIds, self.graph, check=False) 

608 

609 def difference(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

610 """Return a new set with all data IDs in this that are not in other. 

611 

612 Parameters 

613 ---------- 

614 other : `DataCoordinateIterable` 

615 An iterable of data IDs with ``other.graph == self.graph``. 

616 

617 Returns 

618 ------- 

619 intersection : `DataCoordinateSet` 

620 A new `DataCoordinateSet` instance. 

621 """ 

622 if self.graph != other.graph: 

623 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

624 return DataCoordinateSet(self._dataIds - other.toSet()._dataIds, self.graph, check=False) 

625 

626 def toSet(self) -> DataCoordinateSet: 

627 # Docstring inherited from DataCoordinateIterable. 

628 return self 

629 

630 def subset(self, graph: DimensionGraph) -> DataCoordinateSet: 

631 """Return a set whose data IDs identify a subset. 

632 

633 Parameters 

634 ---------- 

635 graph : `DimensionGraph` 

636 Dimensions to be identified by the data IDs in the returned 

637 iterable. Must be a subset of ``self.graph``. 

638 

639 Returns 

640 ------- 

641 set : `DataCoordinateSet` 

642 A `DataCoordinateSet` with ``set.graph == graph``. 

643 Will be ``self`` if ``graph == self.graph``. Elements are 

644 equivalent to those that would be created by calling 

645 `DataCoordinate.subset` on all elements in ``self``, with 

646 deduplication but and in arbitrary order. 

647 """ 

648 if graph == self.graph: 

649 return self 

650 return DataCoordinateSet( 

651 {dataId.subset(graph) for dataId in self._dataIds}, graph, **self._subsetKwargs(graph) 

652 ) 

653 

654 

655class DataCoordinateSequence(_DataCoordinateCollectionBase, Sequence[DataCoordinate]): 

656 """Iterable supporting the full Sequence interface. 

657 

658 A `DataCoordinateIterable` implementation that supports the full 

659 `collections.abc.Sequence` interface. 

660 

661 Parameters 

662 ---------- 

663 dataIds : `collections.abc.Sequence` [ `DataCoordinate` ] 

664 A sequence of `DataCoordinate` instances, with dimensions equal to 

665 ``graph``. 

666 graph : `DimensionGraph` 

667 Dimensions identified by all data IDs in the set. 

668 hasFull : `bool`, optional 

669 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

670 `True` for all given data IDs. If `False`, no such guarantee is made, 

671 and `DataCoordinateSet.hasFull` will always return `False`. If `None` 

672 (default), `DataCoordinateSet.hasFull` will be computed from the given 

673 data IDs, immediately if ``check`` is `True`, or on first use if 

674 ``check`` is `False`. 

675 hasRecords : `bool`, optional 

676 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

677 returns `True` for all given data IDs. If `False`, no such guarantee 

678 is made and `DataCoordinateSet.hasRecords` will always return `False`. 

679 If `None` (default), `DataCoordinateSet.hasRecords` will be computed 

680 from the given data IDs, immediately if ``check`` is `True`, or on 

681 first use if ``check`` is `False`. 

682 check: `bool`, optional 

683 If `True` (default) check that all data IDs are consistent with the 

684 given ``graph`` and state flags at construction. If `False`, no 

685 checking will occur. 

686 """ 

687 

688 def __init__( 

689 self, 

690 dataIds: Sequence[DataCoordinate], 

691 graph: DimensionGraph, 

692 *, 

693 hasFull: bool | None = None, 

694 hasRecords: bool | None = None, 

695 check: bool = True, 

696 ): 

697 super().__init__(tuple(dataIds), graph, hasFull=hasFull, hasRecords=hasRecords, check=check) 

698 

699 _dataIds: Sequence[DataCoordinate] 

700 

701 __slots__ = () 

702 

703 def __str__(self) -> str: 

704 return str(tuple(self._dataIds)) 

705 

706 def __repr__(self) -> str: 

707 return ( 

708 f"DataCoordinateSequence({tuple(self._dataIds)}, {self._graph!r}, " 

709 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})" 

710 ) 

711 

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

713 if isinstance(other, DataCoordinateSequence): 

714 return self._graph == other._graph and self._dataIds == other._dataIds 

715 return False 

716 

717 @overload 

718 def __getitem__(self, index: int) -> DataCoordinate: 

719 pass 

720 

721 @overload 

722 def __getitem__(self, index: slice) -> DataCoordinateSequence: 

723 pass 

724 

725 def __getitem__(self, index: Any) -> Any: 

726 r = self._dataIds[index] 

727 if isinstance(index, slice): 

728 return DataCoordinateSequence( 

729 r, self._graph, hasFull=self._hasFull, hasRecords=self._hasRecords, check=False 

730 ) 

731 return r 

732 

733 def toSequence(self) -> DataCoordinateSequence: 

734 # Docstring inherited from DataCoordinateIterable. 

735 return self 

736 

737 def subset(self, graph: DimensionGraph) -> DataCoordinateSequence: 

738 """Return a sequence whose data IDs identify a subset. 

739 

740 Parameters 

741 ---------- 

742 graph : `DimensionGraph` 

743 Dimensions to be identified by the data IDs in the returned 

744 iterable. Must be a subset of ``self.graph``. 

745 

746 Returns 

747 ------- 

748 set : `DataCoordinateSequence` 

749 A `DataCoordinateSequence` with ``set.graph == graph``. 

750 Will be ``self`` if ``graph == self.graph``. Elements are 

751 equivalent to those that would be created by calling 

752 `DataCoordinate.subset` on all elements in ``self``, in the same 

753 order and with no deduplication. 

754 """ 

755 if graph == self.graph: 

756 return self 

757 return DataCoordinateSequence( 

758 tuple(dataId.subset(graph) for dataId in self._dataIds), graph, **self._subsetKwargs(graph) 

759 )