Coverage for python/lsst/daf/butler/core/dimensions/_dataCoordinateIterable.py: 30%

215 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 22:06 -0800

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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ( 

25 "DataCoordinateIterable", 

26 "DataCoordinateSet", 

27 "DataCoordinateSequence", 

28) 

29 

30from abc import abstractmethod 

31from typing import ( 

32 AbstractSet, 

33 Any, 

34 Callable, 

35 Collection, 

36 Dict, 

37 Iterable, 

38 Iterator, 

39 List, 

40 Optional, 

41 Sequence, 

42 overload, 

43) 

44 

45import sqlalchemy 

46 

47from ..simpleQuery import SimpleQuery 

48from ._coordinate import DataCoordinate 

49from ._graph import DimensionGraph 

50from ._universe import DimensionUniverse 

51 

52 

53class DataCoordinateIterable(Iterable[DataCoordinate]): 

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

55 

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

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

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

59 """ 

60 

61 __slots__ = () 

62 

63 @staticmethod 

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

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

66 

67 Parameters 

68 ---------- 

69 dataId : `DataCoordinate` 

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

71 an arbitrary mapping. No runtime checking is performed. 

72 

73 Returns 

74 ------- 

75 iterable : `DataCoordinateIterable` 

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

77 implementation-detail) subclass. Guaranteed to implement 

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

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

80 well as that of `DataCoordinateIterable`. 

81 """ 

82 return _ScalarDataCoordinateIterable(dataId) 

83 

84 @property 

85 @abstractmethod 

86 def graph(self) -> DimensionGraph: 

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

88 raise NotImplementedError() 

89 

90 @property 

91 def universe(self) -> DimensionUniverse: 

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

93 

94 (`DimensionUniverse`). 

95 """ 

96 return self.graph.universe 

97 

98 @abstractmethod 

99 def hasFull(self) -> bool: 

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

101 

102 Not just required dimensions. 

103 

104 Returns 

105 ------- 

106 state : `bool` 

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

108 If `False`, no guarantees are made. 

109 """ 

110 raise NotImplementedError() 

111 

112 @abstractmethod 

113 def hasRecords(self) -> bool: 

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

115 

116 Returns 

117 ------- 

118 state : `bool` 

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

120 If `False`, no guarantees are made. 

121 """ 

122 raise NotImplementedError() 

123 

124 def toSet(self) -> DataCoordinateSet: 

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

126 

127 Returns 

128 ------- 

129 set : `DataCoordinateSet` 

130 A `DatasetCoordinateSet` instance with the same elements as 

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

132 already a `DataCoordinateSet`. 

133 """ 

134 return DataCoordinateSet( 

135 frozenset(self), 

136 graph=self.graph, 

137 hasFull=self.hasFull(), 

138 hasRecords=self.hasRecords(), 

139 check=False, 

140 ) 

141 

142 def toSequence(self) -> DataCoordinateSequence: 

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

144 

145 Returns 

146 ------- 

147 seq : `DataCoordinateSequence` 

148 A new `DatasetCoordinateSequence` with the same elements as 

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

150 `DataCoordinateSequence`. 

151 """ 

152 return DataCoordinateSequence( 

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

154 ) 

155 

156 def constrain(self, query: SimpleQuery, columns: Callable[[str], sqlalchemy.sql.ColumnElement]) -> None: 

157 """Constrain a SQL query to include or relate to only known data IDs. 

158 

159 Parameters 

160 ---------- 

161 query : `SimpleQuery` 

162 Struct that represents the SQL query to constrain, either by 

163 appending to its WHERE clause, joining a new table or subquery, 

164 or both. 

165 columns : `Callable` 

166 A callable that accepts `str` dimension names and returns 

167 SQLAlchemy objects representing a column for that dimension's 

168 primary key value in the query. 

169 """ 

170 toOrTogether: List[sqlalchemy.sql.ColumnElement] = [] 

171 for dataId in self: 

172 toOrTogether.append( 

173 sqlalchemy.sql.and_( 

174 *[columns(dimension.name) == dataId[dimension.name] for dimension in self.graph.required] 

175 ) 

176 ) 

177 query.where.append(sqlalchemy.sql.or_(*toOrTogether)) 

178 

179 @abstractmethod 

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

181 """Return a subset iterable. 

182 

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

184 dimensions that this one's do. 

185 

186 Parameters 

187 ---------- 

188 graph : `DimensionGraph` 

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

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

191 

192 Returns 

193 ------- 

194 iterable : `DataCoordinateIterable` 

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

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

197 equivalent to those that would be created by calling 

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

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

200 which may make more specific guarantees). 

201 """ 

202 raise NotImplementedError() 

203 

204 

205class _ScalarDataCoordinateIterable(DataCoordinateIterable): 

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

207 

208 A `DataCoordinateIterable` implementation that adapts a single 

209 `DataCoordinate` instance. 

210 

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

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

213 the `DataCoordinateIterable` interface. 

214 

215 Parameters 

216 ---------- 

217 dataId : `DataCoordinate` 

218 The data ID to adapt. 

219 """ 

220 

221 def __init__(self, dataId: DataCoordinate): 

222 self._dataId = dataId 

223 

224 __slots__ = ("_dataId",) 

225 

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

227 yield self._dataId 

228 

229 def __len__(self) -> int: 

230 return 1 

231 

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

233 if isinstance(key, DataCoordinate): 

234 return key == self._dataId 

235 else: 

236 return False 

237 

238 @property 

239 def graph(self) -> DimensionGraph: 

240 # Docstring inherited from DataCoordinateIterable. 

241 return self._dataId.graph 

242 

243 def hasFull(self) -> bool: 

244 # Docstring inherited from DataCoordinateIterable. 

245 return self._dataId.hasFull() 

246 

247 def hasRecords(self) -> bool: 

248 # Docstring inherited from DataCoordinateIterable. 

249 return self._dataId.hasRecords() 

250 

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

252 # Docstring inherited from DataCoordinateIterable. 

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

254 

255 

256class _DataCoordinateCollectionBase(DataCoordinateIterable): 

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

258 

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

260 native Python collection. 

261 

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

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

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

265 

266 Parameters 

267 ---------- 

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

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

270 ``graph``. 

271 graph : `DimensionGraph` 

272 Dimensions identified by all data IDs in the set. 

273 hasFull : `bool`, optional 

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

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

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

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

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

279 hasRecords : `bool`, optional 

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

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

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

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

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

285 `False`. 

286 check: `bool`, optional 

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

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

289 checking will occur. 

290 """ 

291 

292 def __init__( 

293 self, 

294 dataIds: Collection[DataCoordinate], 

295 graph: DimensionGraph, 

296 *, 

297 hasFull: Optional[bool] = None, 

298 hasRecords: Optional[bool] = None, 

299 check: bool = True, 

300 ): 

301 self._dataIds = dataIds 

302 self._graph = graph 

303 if check: 

304 for dataId in self._dataIds: 

305 if hasFull and not dataId.hasFull(): 

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

307 if hasRecords and not dataId.hasRecords(): 

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

309 if dataId.graph != self._graph: 

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

311 if hasFull is None: 

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

313 if hasRecords is None: 

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

315 self._hasFull = hasFull 

316 self._hasRecords = hasRecords 

317 

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

319 

320 @property 

321 def graph(self) -> DimensionGraph: 

322 # Docstring inherited from DataCoordinateIterable. 

323 return self._graph 

324 

325 def hasFull(self) -> bool: 

326 # Docstring inherited from DataCoordinateIterable. 

327 if self._hasFull is None: 

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

329 return self._hasFull 

330 

331 def hasRecords(self) -> bool: 

332 # Docstring inherited from DataCoordinateIterable. 

333 if self._hasRecords is None: 

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

335 return self._hasRecords 

336 

337 def toSet(self) -> DataCoordinateSet: 

338 # Docstring inherited from DataCoordinateIterable. 

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

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

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

342 return DataCoordinateSet( 

343 frozenset(self._dataIds), 

344 graph=self._graph, 

345 hasFull=self._hasFull, 

346 hasRecords=self._hasRecords, 

347 check=False, 

348 ) 

349 

350 def toSequence(self) -> DataCoordinateSequence: 

351 # Docstring inherited from DataCoordinateIterable. 

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

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

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

355 return DataCoordinateSequence( 

356 tuple(self._dataIds), 

357 graph=self._graph, 

358 hasFull=self._hasFull, 

359 hasRecords=self._hasRecords, 

360 check=False, 

361 ) 

362 

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

364 return iter(self._dataIds) 

365 

366 def __len__(self) -> int: 

367 return len(self._dataIds) 

368 

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

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

371 return key in self._dataIds 

372 

373 def _subsetKwargs(self, graph: DimensionGraph) -> Dict[str, Any]: 

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

375 

376 Parameters 

377 ---------- 

378 graph : `DimensionGraph` 

379 Dimensions passed to `subset`. 

380 

381 Returns 

382 ------- 

383 **kwargs 

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

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

386 dimensions. 

387 """ 

388 hasFull: Optional[bool] 

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

390 hasFull = True 

391 else: 

392 hasFull = self._hasFull 

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

394 

395 

396class DataCoordinateSet(_DataCoordinateCollectionBase): 

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

398 

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

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

401 

402 Parameters 

403 ---------- 

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

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

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

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

408 graph : `DimensionGraph` 

409 Dimensions identified by all data IDs in the set. 

410 hasFull : `bool`, optional 

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

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

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

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

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

416 ``check`` is `False`. 

417 hasRecords : `bool`, optional 

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

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

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

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

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

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

424 check: `bool`, optional 

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

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

427 checking will occur. 

428 

429 Notes 

430 ----- 

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

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

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

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

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

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

437 the same dimensions. In particular: 

438 

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

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

441 same elements; 

442 

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

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

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

446 

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

448 be a `DataCoordinateIterable` with the same dimensions; 

449 

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

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

452 dimensions _and_ the same ``dtype``; 

453 

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

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

456 `DataCoordinateIterable` with the same dimensions _and_ the same 

457 ``dtype``. 

458 

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

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

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

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

463 elements are contributed by each operand). 

464 """ 

465 

466 def __init__( 

467 self, 

468 dataIds: AbstractSet[DataCoordinate], 

469 graph: DimensionGraph, 

470 *, 

471 hasFull: Optional[bool] = None, 

472 hasRecords: Optional[bool] = None, 

473 check: bool = True, 

474 ): 

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

476 

477 _dataIds: AbstractSet[DataCoordinate] 

478 

479 __slots__ = () 

480 

481 def __str__(self) -> str: 

482 return str(set(self._dataIds)) 

483 

484 def __repr__(self) -> str: 

485 return ( 

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

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

488 ) 

489 

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

491 if isinstance(other, DataCoordinateSet): 

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

493 return False 

494 

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

496 if self.graph != other.graph: 

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

498 return self._dataIds <= other._dataIds 

499 

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

501 if self.graph != other.graph: 

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

503 return self._dataIds >= other._dataIds 

504 

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

506 if self.graph != other.graph: 

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

508 return self._dataIds < other._dataIds 

509 

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

511 if self.graph != other.graph: 

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

513 return self._dataIds > other._dataIds 

514 

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

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

517 

518 Parameters 

519 ---------- 

520 other : `DataCoordinateIterable` 

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

522 

523 Returns 

524 ------- 

525 issubset : `bool` 

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

527 `False` otherwise. 

528 """ 

529 if self.graph != other.graph: 

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

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

532 

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

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

535 

536 Parameters 

537 ---------- 

538 other : `DataCoordinateIterable` 

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

540 

541 Returns 

542 ------- 

543 issuperset : `bool` 

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

545 `False` otherwise. 

546 """ 

547 if self.graph != other.graph: 

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

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

550 

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

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

553 

554 Parameters 

555 ---------- 

556 other : `DataCoordinateIterable` 

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

558 

559 Returns 

560 ------- 

561 isdisjoint : `bool` 

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

563 `False` otherwise. 

564 """ 

565 if self.graph != other.graph: 

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

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

568 

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

570 if self.graph != other.graph: 

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

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

573 

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

575 if self.graph != other.graph: 

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

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

578 

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

580 if self.graph != other.graph: 

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

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

583 

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

585 if self.graph != other.graph: 

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

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

588 

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

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

591 

592 Parameters 

593 ---------- 

594 other : `DataCoordinateIterable` 

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

596 

597 Returns 

598 ------- 

599 intersection : `DataCoordinateSet` 

600 A new `DataCoordinateSet` instance. 

601 """ 

602 if self.graph != other.graph: 

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

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

605 

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

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

608 

609 Parameters 

610 ---------- 

611 other : `DataCoordinateIterable` 

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

613 

614 Returns 

615 ------- 

616 intersection : `DataCoordinateSet` 

617 A new `DataCoordinateSet` instance. 

618 """ 

619 if self.graph != other.graph: 

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

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

622 

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

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

625 

626 Parameters 

627 ---------- 

628 other : `DataCoordinateIterable` 

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

630 

631 Returns 

632 ------- 

633 intersection : `DataCoordinateSet` 

634 A new `DataCoordinateSet` instance. 

635 """ 

636 if self.graph != other.graph: 

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

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

639 

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

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

642 

643 Parameters 

644 ---------- 

645 other : `DataCoordinateIterable` 

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

647 

648 Returns 

649 ------- 

650 intersection : `DataCoordinateSet` 

651 A new `DataCoordinateSet` instance. 

652 """ 

653 if self.graph != other.graph: 

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

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

656 

657 def toSet(self) -> DataCoordinateSet: 

658 # Docstring inherited from DataCoordinateIterable. 

659 return self 

660 

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

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

663 

664 Parameters 

665 ---------- 

666 graph : `DimensionGraph` 

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

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

669 

670 Returns 

671 ------- 

672 set : `DataCoordinateSet` 

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

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

675 equivalent to those that would be created by calling 

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

677 deduplication but and in arbitrary order. 

678 """ 

679 if graph == self.graph: 

680 return self 

681 return DataCoordinateSet( 

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

683 ) 

684 

685 

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

687 """Iterable supporting the full Sequence interface. 

688 

689 A `DataCoordinateIterable` implementation that supports the full 

690 `collections.abc.Sequence` interface. 

691 

692 Parameters 

693 ---------- 

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

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

696 ``graph``. 

697 graph : `DimensionGraph` 

698 Dimensions identified by all data IDs in the set. 

699 hasFull : `bool`, optional 

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

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

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

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

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

705 ``check`` is `False`. 

706 hasRecords : `bool`, optional 

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

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

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

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

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

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

713 check: `bool`, optional 

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

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

716 checking will occur. 

717 """ 

718 

719 def __init__( 

720 self, 

721 dataIds: Sequence[DataCoordinate], 

722 graph: DimensionGraph, 

723 *, 

724 hasFull: Optional[bool] = None, 

725 hasRecords: Optional[bool] = None, 

726 check: bool = True, 

727 ): 

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

729 

730 _dataIds: Sequence[DataCoordinate] 

731 

732 __slots__ = () 

733 

734 def __str__(self) -> str: 

735 return str(tuple(self._dataIds)) 

736 

737 def __repr__(self) -> str: 

738 return ( 

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

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

741 ) 

742 

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

744 if isinstance(other, DataCoordinateSequence): 

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

746 return False 

747 

748 @overload 

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

750 pass 

751 

752 @overload # noqa: F811 (FIXME: remove for py 3.8+) 

753 def __getitem__(self, index: slice) -> DataCoordinateSequence: # noqa: F811 

754 pass 

755 

756 def __getitem__(self, index: Any) -> Any: # noqa: F811 

757 r = self._dataIds[index] 

758 if isinstance(index, slice): 

759 return DataCoordinateSequence( 

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

761 ) 

762 return r 

763 

764 def toSequence(self) -> DataCoordinateSequence: 

765 # Docstring inherited from DataCoordinateIterable. 

766 return self 

767 

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

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

770 

771 Parameters 

772 ---------- 

773 graph : `DimensionGraph` 

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

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

776 

777 Returns 

778 ------- 

779 set : `DataCoordinateSequence` 

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

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

782 equivalent to those that would be created by calling 

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

784 order and with no deduplication. 

785 """ 

786 if graph == self.graph: 

787 return self 

788 return DataCoordinateSequence( 

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

790 )