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

1import collections 

2import re 

3 

4from sqlalchemy import util as sqlautil 

5 

6from .. import util 

7from ..util import compat 

8 

9_relative_destination = re.compile(r"(?:(.+?)@)?(\w+)?((?:\+|-)\d+)") 

10_revision_illegal_chars = ["@", "-", "+"] 

11 

12 

13class RevisionError(Exception): 

14 pass 

15 

16 

17class RangeNotAncestorError(RevisionError): 

18 def __init__(self, lower, upper): 

19 self.lower = lower 

20 self.upper = upper 

21 super(RangeNotAncestorError, self).__init__( 

22 "Revision %s is not an ancestor of revision %s" 

23 % (lower or "base", upper or "base") 

24 ) 

25 

26 

27class MultipleHeads(RevisionError): 

28 def __init__(self, heads, argument): 

29 self.heads = heads 

30 self.argument = argument 

31 super(MultipleHeads, self).__init__( 

32 "Multiple heads are present for given argument '%s'; " 

33 "%s" % (argument, ", ".join(heads)) 

34 ) 

35 

36 

37class ResolutionError(RevisionError): 

38 def __init__(self, message, argument): 

39 super(ResolutionError, self).__init__(message) 

40 self.argument = argument 

41 

42 

43class RevisionMap(object): 

44 """Maintains a map of :class:`.Revision` objects. 

45 

46 :class:`.RevisionMap` is used by :class:`.ScriptDirectory` to maintain 

47 and traverse the collection of :class:`.Script` objects, which are 

48 themselves instances of :class:`.Revision`. 

49 

50 """ 

51 

52 def __init__(self, generator): 

53 """Construct a new :class:`.RevisionMap`. 

54 

55 :param generator: a zero-arg callable that will generate an iterable 

56 of :class:`.Revision` instances to be used. These are typically 

57 :class:`.Script` subclasses within regular Alembic use. 

58 

59 """ 

60 self._generator = generator 

61 

62 @util.memoized_property 

63 def heads(self): 

64 """All "head" revisions as strings. 

65 

66 This is normally a tuple of length one, 

67 unless unmerged branches are present. 

68 

69 :return: a tuple of string revision numbers. 

70 

71 """ 

72 self._revision_map 

73 return self.heads 

74 

75 @util.memoized_property 

76 def bases(self): 

77 """All "base" revisions as strings. 

78 

79 These are revisions that have a ``down_revision`` of None, 

80 or empty tuple. 

81 

82 :return: a tuple of string revision numbers. 

83 

84 """ 

85 self._revision_map 

86 return self.bases 

87 

88 @util.memoized_property 

89 def _real_heads(self): 

90 """All "real" head revisions as strings. 

91 

92 :return: a tuple of string revision numbers. 

93 

94 """ 

95 self._revision_map 

96 return self._real_heads 

97 

98 @util.memoized_property 

99 def _real_bases(self): 

100 """All "real" base revisions as strings. 

101 

102 :return: a tuple of string revision numbers. 

103 

104 """ 

105 self._revision_map 

106 return self._real_bases 

107 

108 @util.memoized_property 

109 def _revision_map(self): 

110 """memoized attribute, initializes the revision map from the 

111 initial collection. 

112 

113 """ 

114 map_ = {} 

115 

116 heads = sqlautil.OrderedSet() 

117 _real_heads = sqlautil.OrderedSet() 

118 self.bases = () 

119 self._real_bases = () 

120 

121 has_branch_labels = set() 

122 has_depends_on = set() 

123 for revision in self._generator(): 

124 

125 if revision.revision in map_: 

126 util.warn( 

127 "Revision %s is present more than once" % revision.revision 

128 ) 

129 map_[revision.revision] = revision 

130 if revision.branch_labels: 

131 has_branch_labels.add(revision) 

132 if revision.dependencies: 

133 has_depends_on.add(revision) 

134 heads.add(revision.revision) 

135 _real_heads.add(revision.revision) 

136 if revision.is_base: 

137 self.bases += (revision.revision,) 

138 if revision._is_real_base: 

139 self._real_bases += (revision.revision,) 

140 

141 # add the branch_labels to the map_. We'll need these 

142 # to resolve the dependencies. 

143 for revision in has_branch_labels: 

144 self._map_branch_labels(revision, map_) 

145 

146 for revision in has_depends_on: 

147 self._add_depends_on(revision, map_) 

148 

149 for rev in map_.values(): 

150 for downrev in rev._all_down_revisions: 

151 if downrev not in map_: 

152 util.warn( 

153 "Revision %s referenced from %s is not present" 

154 % (downrev, rev) 

155 ) 

156 down_revision = map_[downrev] 

157 down_revision.add_nextrev(rev) 

158 if downrev in rev._versioned_down_revisions: 

159 heads.discard(downrev) 

160 _real_heads.discard(downrev) 

161 

162 map_[None] = map_[()] = None 

163 self.heads = tuple(heads) 

164 self._real_heads = tuple(_real_heads) 

165 

166 for revision in has_branch_labels: 

167 self._add_branches(revision, map_, map_branch_labels=False) 

168 return map_ 

169 

170 def _map_branch_labels(self, revision, map_): 

171 if revision.branch_labels: 

172 for branch_label in revision._orig_branch_labels: 

173 if branch_label in map_: 

174 raise RevisionError( 

175 "Branch name '%s' in revision %s already " 

176 "used by revision %s" 

177 % ( 

178 branch_label, 

179 revision.revision, 

180 map_[branch_label].revision, 

181 ) 

182 ) 

183 map_[branch_label] = revision 

184 

185 def _add_branches(self, revision, map_, map_branch_labels=True): 

186 if map_branch_labels: 

187 self._map_branch_labels(revision, map_) 

188 

189 if revision.branch_labels: 

190 revision.branch_labels.update(revision.branch_labels) 

191 for node in self._get_descendant_nodes( 

192 [revision], map_, include_dependencies=False 

193 ): 

194 node.branch_labels.update(revision.branch_labels) 

195 

196 parent = node 

197 while ( 

198 parent 

199 and not parent._is_real_branch_point 

200 and not parent.is_merge_point 

201 ): 

202 

203 parent.branch_labels.update(revision.branch_labels) 

204 if parent.down_revision: 

205 parent = map_[parent.down_revision] 

206 else: 

207 break 

208 

209 def _add_depends_on(self, revision, map_): 

210 if revision.dependencies: 

211 deps = [map_[dep] for dep in util.to_tuple(revision.dependencies)] 

212 revision._resolved_dependencies = tuple([d.revision for d in deps]) 

213 

214 def add_revision(self, revision, _replace=False): 

215 """add a single revision to an existing map. 

216 

217 This method is for single-revision use cases, it's not 

218 appropriate for fully populating an entire revision map. 

219 

220 """ 

221 map_ = self._revision_map 

222 if not _replace and revision.revision in map_: 

223 util.warn( 

224 "Revision %s is present more than once" % revision.revision 

225 ) 

226 elif _replace and revision.revision not in map_: 

227 raise Exception("revision %s not in map" % revision.revision) 

228 

229 map_[revision.revision] = revision 

230 self._add_branches(revision, map_) 

231 self._add_depends_on(revision, map_) 

232 

233 if revision.is_base: 

234 self.bases += (revision.revision,) 

235 if revision._is_real_base: 

236 self._real_bases += (revision.revision,) 

237 for downrev in revision._all_down_revisions: 

238 if downrev not in map_: 

239 util.warn( 

240 "Revision %s referenced from %s is not present" 

241 % (downrev, revision) 

242 ) 

243 map_[downrev].add_nextrev(revision) 

244 if revision._is_real_head: 

245 self._real_heads = tuple( 

246 head 

247 for head in self._real_heads 

248 if head 

249 not in set(revision._all_down_revisions).union( 

250 [revision.revision] 

251 ) 

252 ) + (revision.revision,) 

253 if revision.is_head: 

254 self.heads = tuple( 

255 head 

256 for head in self.heads 

257 if head 

258 not in set(revision._versioned_down_revisions).union( 

259 [revision.revision] 

260 ) 

261 ) + (revision.revision,) 

262 

263 def get_current_head(self, branch_label=None): 

264 """Return the current head revision. 

265 

266 If the script directory has multiple heads 

267 due to branching, an error is raised; 

268 :meth:`.ScriptDirectory.get_heads` should be 

269 preferred. 

270 

271 :param branch_label: optional branch name which will limit the 

272 heads considered to those which include that branch_label. 

273 

274 :return: a string revision number. 

275 

276 .. seealso:: 

277 

278 :meth:`.ScriptDirectory.get_heads` 

279 

280 """ 

281 current_heads = self.heads 

282 if branch_label: 

283 current_heads = self.filter_for_lineage( 

284 current_heads, branch_label 

285 ) 

286 if len(current_heads) > 1: 

287 raise MultipleHeads( 

288 current_heads, 

289 "%s@head" % branch_label if branch_label else "head", 

290 ) 

291 

292 if current_heads: 

293 return current_heads[0] 

294 else: 

295 return None 

296 

297 def _get_base_revisions(self, identifier): 

298 return self.filter_for_lineage(self.bases, identifier) 

299 

300 def get_revisions(self, id_): 

301 """Return the :class:`.Revision` instances with the given rev id 

302 or identifiers. 

303 

304 May be given a single identifier, a sequence of identifiers, or the 

305 special symbols "head" or "base". The result is a tuple of one 

306 or more identifiers, or an empty tuple in the case of "base". 

307 

308 In the cases where 'head', 'heads' is requested and the 

309 revision map is empty, returns an empty tuple. 

310 

311 Supports partial identifiers, where the given identifier 

312 is matched against all identifiers that start with the given 

313 characters; if there is exactly one match, that determines the 

314 full revision. 

315 

316 """ 

317 

318 if isinstance(id_, (list, tuple, set, frozenset)): 

319 return sum([self.get_revisions(id_elem) for id_elem in id_], ()) 

320 else: 

321 resolved_id, branch_label = self._resolve_revision_number(id_) 

322 return tuple( 

323 self._revision_for_ident(rev_id, branch_label) 

324 for rev_id in resolved_id 

325 ) 

326 

327 def get_revision(self, id_): 

328 """Return the :class:`.Revision` instance with the given rev id. 

329 

330 If a symbolic name such as "head" or "base" is given, resolves 

331 the identifier into the current head or base revision. If the symbolic 

332 name refers to multiples, :class:`.MultipleHeads` is raised. 

333 

334 Supports partial identifiers, where the given identifier 

335 is matched against all identifiers that start with the given 

336 characters; if there is exactly one match, that determines the 

337 full revision. 

338 

339 """ 

340 

341 resolved_id, branch_label = self._resolve_revision_number(id_) 

342 if len(resolved_id) > 1: 

343 raise MultipleHeads(resolved_id, id_) 

344 elif resolved_id: 

345 resolved_id = resolved_id[0] 

346 

347 return self._revision_for_ident(resolved_id, branch_label) 

348 

349 def _resolve_branch(self, branch_label): 

350 try: 

351 branch_rev = self._revision_map[branch_label] 

352 except KeyError: 

353 try: 

354 nonbranch_rev = self._revision_for_ident(branch_label) 

355 except ResolutionError: 

356 raise ResolutionError( 

357 "No such branch: '%s'" % branch_label, branch_label 

358 ) 

359 else: 

360 return nonbranch_rev 

361 else: 

362 return branch_rev 

363 

364 def _revision_for_ident(self, resolved_id, check_branch=None): 

365 if check_branch: 

366 branch_rev = self._resolve_branch(check_branch) 

367 else: 

368 branch_rev = None 

369 

370 try: 

371 revision = self._revision_map[resolved_id] 

372 except KeyError: 

373 # break out to avoid misleading py3k stack traces 

374 revision = False 

375 if revision is False: 

376 # do a partial lookup 

377 revs = [ 

378 x 

379 for x in self._revision_map 

380 if x and len(x) > 3 and x.startswith(resolved_id) 

381 ] 

382 

383 if branch_rev: 

384 revs = self.filter_for_lineage(revs, check_branch) 

385 if not revs: 

386 raise ResolutionError( 

387 "No such revision or branch '%s'%s" 

388 % ( 

389 resolved_id, 

390 ( 

391 "; please ensure at least four characters are " 

392 "present for partial revision identifier matches" 

393 if len(resolved_id) < 4 

394 else "" 

395 ), 

396 ), 

397 resolved_id, 

398 ) 

399 elif len(revs) > 1: 

400 raise ResolutionError( 

401 "Multiple revisions start " 

402 "with '%s': %s..." 

403 % (resolved_id, ", ".join("'%s'" % r for r in revs[0:3])), 

404 resolved_id, 

405 ) 

406 else: 

407 revision = self._revision_map[revs[0]] 

408 

409 if check_branch and revision is not None: 

410 if not self._shares_lineage( 

411 revision.revision, branch_rev.revision 

412 ): 

413 raise ResolutionError( 

414 "Revision %s is not a member of branch '%s'" 

415 % (revision.revision, check_branch), 

416 resolved_id, 

417 ) 

418 return revision 

419 

420 def _filter_into_branch_heads(self, targets): 

421 targets = set(targets) 

422 

423 for rev in list(targets): 

424 if targets.intersection( 

425 self._get_descendant_nodes([rev], include_dependencies=False) 

426 ).difference([rev]): 

427 targets.discard(rev) 

428 return targets 

429 

430 def filter_for_lineage( 

431 self, targets, check_against, include_dependencies=False 

432 ): 

433 id_, branch_label = self._resolve_revision_number(check_against) 

434 

435 shares = [] 

436 if branch_label: 

437 shares.append(branch_label) 

438 if id_: 

439 shares.extend(id_) 

440 

441 return [ 

442 tg 

443 for tg in targets 

444 if self._shares_lineage( 

445 tg, shares, include_dependencies=include_dependencies 

446 ) 

447 ] 

448 

449 def _shares_lineage( 

450 self, target, test_against_revs, include_dependencies=False 

451 ): 

452 if not test_against_revs: 

453 return True 

454 if not isinstance(target, Revision): 

455 target = self._revision_for_ident(target) 

456 

457 test_against_revs = [ 

458 self._revision_for_ident(test_against_rev) 

459 if not isinstance(test_against_rev, Revision) 

460 else test_against_rev 

461 for test_against_rev in util.to_tuple( 

462 test_against_revs, default=() 

463 ) 

464 ] 

465 

466 return bool( 

467 set( 

468 self._get_descendant_nodes( 

469 [target], include_dependencies=include_dependencies 

470 ) 

471 ) 

472 .union( 

473 self._get_ancestor_nodes( 

474 [target], include_dependencies=include_dependencies 

475 ) 

476 ) 

477 .intersection(test_against_revs) 

478 ) 

479 

480 def _resolve_revision_number(self, id_): 

481 if isinstance(id_, compat.string_types) and "@" in id_: 

482 branch_label, id_ = id_.split("@", 1) 

483 

484 elif id_ is not None and ( 

485 ( 

486 isinstance(id_, tuple) 

487 and id_ 

488 and not isinstance(id_[0], compat.string_types) 

489 ) 

490 or not isinstance(id_, compat.string_types + (tuple,)) 

491 ): 

492 raise RevisionError( 

493 "revision identifier %r is not a string; ensure database " 

494 "driver settings are correct" % (id_,) 

495 ) 

496 

497 else: 

498 branch_label = None 

499 

500 # ensure map is loaded 

501 self._revision_map 

502 if id_ == "heads": 

503 if branch_label: 

504 return ( 

505 self.filter_for_lineage(self.heads, branch_label), 

506 branch_label, 

507 ) 

508 else: 

509 return self._real_heads, branch_label 

510 elif id_ == "head": 

511 current_head = self.get_current_head(branch_label) 

512 if current_head: 

513 return (current_head,), branch_label 

514 else: 

515 return (), branch_label 

516 elif id_ == "base" or id_ is None: 

517 return (), branch_label 

518 else: 

519 return util.to_tuple(id_, default=None), branch_label 

520 

521 def _relative_iterate( 

522 self, 

523 destination, 

524 source, 

525 is_upwards, 

526 implicit_base, 

527 inclusive, 

528 assert_relative_length, 

529 ): 

530 if isinstance(destination, compat.string_types): 

531 match = _relative_destination.match(destination) 

532 if not match: 

533 return None 

534 else: 

535 return None 

536 

537 relative = int(match.group(3)) 

538 symbol = match.group(2) 

539 branch_label = match.group(1) 

540 

541 reldelta = 1 if inclusive and not symbol else 0 

542 

543 if is_upwards: 

544 if branch_label: 

545 from_ = "%s@head" % branch_label 

546 elif symbol: 

547 if symbol.startswith("head"): 

548 from_ = symbol 

549 else: 

550 from_ = "%s@head" % symbol 

551 else: 

552 from_ = "head" 

553 to_ = source 

554 else: 

555 if branch_label: 

556 to_ = "%s@base" % branch_label 

557 elif symbol: 

558 to_ = "%s@base" % symbol 

559 else: 

560 to_ = "base" 

561 from_ = source 

562 

563 revs = list( 

564 self._iterate_revisions( 

565 from_, to_, inclusive=inclusive, implicit_base=implicit_base 

566 ) 

567 ) 

568 

569 if symbol: 

570 if branch_label: 

571 symbol_rev = self.get_revision( 

572 "%s@%s" % (branch_label, symbol) 

573 ) 

574 else: 

575 symbol_rev = self.get_revision(symbol) 

576 if symbol.startswith("head"): 

577 index = 0 

578 elif symbol == "base": 

579 index = len(revs) - 1 

580 else: 

581 range_ = compat.range(len(revs) - 1, 0, -1) 

582 for index in range_: 

583 if symbol_rev.revision == revs[index].revision: 

584 break 

585 else: 

586 index = 0 

587 else: 

588 index = 0 

589 if is_upwards: 

590 revs = revs[index - relative - reldelta :] 

591 if ( 

592 not index 

593 and assert_relative_length 

594 and len(revs) < abs(relative - reldelta) 

595 ): 

596 raise RevisionError( 

597 "Relative revision %s didn't " 

598 "produce %d migrations" % (destination, abs(relative)) 

599 ) 

600 else: 

601 revs = revs[0 : index - relative + reldelta] 

602 if ( 

603 not index 

604 and assert_relative_length 

605 and len(revs) != abs(relative) + reldelta 

606 ): 

607 raise RevisionError( 

608 "Relative revision %s didn't " 

609 "produce %d migrations" % (destination, abs(relative)) 

610 ) 

611 

612 return iter(revs) 

613 

614 def iterate_revisions( 

615 self, 

616 upper, 

617 lower, 

618 implicit_base=False, 

619 inclusive=False, 

620 assert_relative_length=True, 

621 select_for_downgrade=False, 

622 ): 

623 """Iterate through script revisions, starting at the given 

624 upper revision identifier and ending at the lower. 

625 

626 The traversal uses strictly the `down_revision` 

627 marker inside each migration script, so 

628 it is a requirement that upper >= lower, 

629 else you'll get nothing back. 

630 

631 The iterator yields :class:`.Revision` objects. 

632 

633 """ 

634 

635 relative_upper = self._relative_iterate( 

636 upper, 

637 lower, 

638 True, 

639 implicit_base, 

640 inclusive, 

641 assert_relative_length, 

642 ) 

643 if relative_upper: 

644 return relative_upper 

645 

646 relative_lower = self._relative_iterate( 

647 lower, 

648 upper, 

649 False, 

650 implicit_base, 

651 inclusive, 

652 assert_relative_length, 

653 ) 

654 if relative_lower: 

655 return relative_lower 

656 

657 return self._iterate_revisions( 

658 upper, 

659 lower, 

660 inclusive=inclusive, 

661 implicit_base=implicit_base, 

662 select_for_downgrade=select_for_downgrade, 

663 ) 

664 

665 def _get_descendant_nodes( 

666 self, 

667 targets, 

668 map_=None, 

669 check=False, 

670 omit_immediate_dependencies=False, 

671 include_dependencies=True, 

672 ): 

673 

674 if omit_immediate_dependencies: 

675 

676 def fn(rev): 

677 if rev not in targets: 

678 return rev._all_nextrev 

679 else: 

680 return rev.nextrev 

681 

682 elif include_dependencies: 

683 

684 def fn(rev): 

685 return rev._all_nextrev 

686 

687 else: 

688 

689 def fn(rev): 

690 return rev.nextrev 

691 

692 return self._iterate_related_revisions( 

693 fn, targets, map_=map_, check=check 

694 ) 

695 

696 def _get_ancestor_nodes( 

697 self, targets, map_=None, check=False, include_dependencies=True 

698 ): 

699 

700 if include_dependencies: 

701 

702 def fn(rev): 

703 return rev._all_down_revisions 

704 

705 else: 

706 

707 def fn(rev): 

708 return rev._versioned_down_revisions 

709 

710 return self._iterate_related_revisions( 

711 fn, targets, map_=map_, check=check 

712 ) 

713 

714 def _iterate_related_revisions(self, fn, targets, map_, check=False): 

715 if map_ is None: 

716 map_ = self._revision_map 

717 

718 seen = set() 

719 todo = collections.deque() 

720 for target in targets: 

721 

722 todo.append(target) 

723 if check: 

724 per_target = set() 

725 

726 while todo: 

727 rev = todo.pop() 

728 if check: 

729 per_target.add(rev) 

730 

731 if rev in seen: 

732 continue 

733 seen.add(rev) 

734 todo.extend(map_[rev_id] for rev_id in fn(rev)) 

735 yield rev 

736 if check: 

737 overlaps = per_target.intersection(targets).difference( 

738 [target] 

739 ) 

740 if overlaps: 

741 raise RevisionError( 

742 "Requested revision %s overlaps with " 

743 "other requested revisions %s" 

744 % ( 

745 target.revision, 

746 ", ".join(r.revision for r in overlaps), 

747 ) 

748 ) 

749 

750 def _iterate_revisions( 

751 self, 

752 upper, 

753 lower, 

754 inclusive=True, 

755 implicit_base=False, 

756 select_for_downgrade=False, 

757 ): 

758 """iterate revisions from upper to lower. 

759 

760 The traversal is depth-first within branches, and breadth-first 

761 across branches as a whole. 

762 

763 """ 

764 

765 requested_lowers = self.get_revisions(lower) 

766 

767 # some complexity to accommodate an iteration where some 

768 # branches are starting from nothing, and others are starting 

769 # from a given point. Additionally, if the bottom branch 

770 # is specified using a branch identifier, then we limit operations 

771 # to just that branch. 

772 

773 limit_to_lower_branch = isinstance( 

774 lower, compat.string_types 

775 ) and lower.endswith("@base") 

776 

777 uppers = util.dedupe_tuple(self.get_revisions(upper)) 

778 

779 if not uppers and not requested_lowers: 

780 return 

781 

782 upper_ancestors = set(self._get_ancestor_nodes(uppers, check=True)) 

783 

784 if limit_to_lower_branch: 

785 lowers = self.get_revisions(self._get_base_revisions(lower)) 

786 elif implicit_base and requested_lowers: 

787 lower_ancestors = set(self._get_ancestor_nodes(requested_lowers)) 

788 lower_descendants = set( 

789 self._get_descendant_nodes(requested_lowers) 

790 ) 

791 base_lowers = set() 

792 candidate_lowers = upper_ancestors.difference( 

793 lower_ancestors 

794 ).difference(lower_descendants) 

795 for rev in candidate_lowers: 

796 for downrev in rev._all_down_revisions: 

797 if self._revision_map[downrev] in candidate_lowers: 

798 break 

799 else: 

800 base_lowers.add(rev) 

801 lowers = base_lowers.union(requested_lowers) 

802 elif implicit_base: 

803 base_lowers = set(self.get_revisions(self._real_bases)) 

804 lowers = base_lowers.union(requested_lowers) 

805 elif not requested_lowers: 

806 lowers = set(self.get_revisions(self._real_bases)) 

807 else: 

808 lowers = requested_lowers 

809 

810 # represents all nodes we will produce 

811 total_space = set( 

812 rev.revision for rev in upper_ancestors 

813 ).intersection( 

814 rev.revision 

815 for rev in self._get_descendant_nodes( 

816 lowers, 

817 check=True, 

818 omit_immediate_dependencies=( 

819 select_for_downgrade and requested_lowers 

820 ), 

821 ) 

822 ) 

823 

824 if not total_space: 

825 # no nodes. determine if this is an invalid range 

826 # or not. 

827 start_from = set(requested_lowers) 

828 start_from.update( 

829 self._get_ancestor_nodes( 

830 list(start_from), include_dependencies=True 

831 ) 

832 ) 

833 

834 # determine all the current branch points represented 

835 # by requested_lowers 

836 start_from = self._filter_into_branch_heads(start_from) 

837 

838 # if the requested start is one of those branch points, 

839 # then just return empty set 

840 if start_from.intersection(upper_ancestors): 

841 return 

842 else: 

843 # otherwise, they requested nodes out of 

844 # order 

845 raise RangeNotAncestorError(lower, upper) 

846 

847 # organize branch points to be consumed separately from 

848 # member nodes 

849 branch_todo = set( 

850 rev 

851 for rev in (self._revision_map[rev] for rev in total_space) 

852 if rev._is_real_branch_point 

853 and len(total_space.intersection(rev._all_nextrev)) > 1 

854 ) 

855 

856 # it's not possible for any "uppers" to be in branch_todo, 

857 # because the ._all_nextrev of those nodes is not in total_space 

858 # assert not branch_todo.intersection(uppers) 

859 

860 todo = collections.deque( 

861 r for r in uppers if r.revision in total_space 

862 ) 

863 

864 # iterate for total_space being emptied out 

865 total_space_modified = True 

866 while total_space: 

867 

868 if not total_space_modified: 

869 raise RevisionError( 

870 "Dependency resolution failed; iteration can't proceed" 

871 ) 

872 total_space_modified = False 

873 # when everything non-branch pending is consumed, 

874 # add to the todo any branch nodes that have no 

875 # descendants left in the queue 

876 if not todo: 

877 todo.extendleft( 

878 sorted( 

879 ( 

880 rev 

881 for rev in branch_todo 

882 if not rev._all_nextrev.intersection(total_space) 

883 ), 

884 # favor "revisioned" branch points before 

885 # dependent ones 

886 key=lambda rev: 0 if rev.is_branch_point else 1, 

887 ) 

888 ) 

889 branch_todo.difference_update(todo) 

890 # iterate nodes that are in the immediate todo 

891 while todo: 

892 rev = todo.popleft() 

893 total_space.remove(rev.revision) 

894 total_space_modified = True 

895 

896 # do depth first for elements within branches, 

897 # don't consume any actual branch nodes 

898 todo.extendleft( 

899 [ 

900 self._revision_map[downrev] 

901 for downrev in reversed(rev._all_down_revisions) 

902 if self._revision_map[downrev] not in branch_todo 

903 and downrev in total_space 

904 ] 

905 ) 

906 

907 if not inclusive and rev in requested_lowers: 

908 continue 

909 yield rev 

910 

911 assert not branch_todo 

912 

913 

914class Revision(object): 

915 """Base class for revisioned objects. 

916 

917 The :class:`.Revision` class is the base of the more public-facing 

918 :class:`.Script` object, which represents a migration script. 

919 The mechanics of revision management and traversal are encapsulated 

920 within :class:`.Revision`, while :class:`.Script` applies this logic 

921 to Python files in a version directory. 

922 

923 """ 

924 

925 nextrev = frozenset() 

926 """following revisions, based on down_revision only.""" 

927 

928 _all_nextrev = frozenset() 

929 

930 revision = None 

931 """The string revision number.""" 

932 

933 down_revision = None 

934 """The ``down_revision`` identifier(s) within the migration script. 

935 

936 Note that the total set of "down" revisions is 

937 down_revision + dependencies. 

938 

939 """ 

940 

941 dependencies = None 

942 """Additional revisions which this revision is dependent on. 

943 

944 From a migration standpoint, these dependencies are added to the 

945 down_revision to form the full iteration. However, the separation 

946 of down_revision from "dependencies" is to assist in navigating 

947 a history that contains many branches, typically a multi-root scenario. 

948 

949 """ 

950 

951 branch_labels = None 

952 """Optional string/tuple of symbolic names to apply to this 

953 revision's branch""" 

954 

955 @classmethod 

956 def verify_rev_id(cls, revision): 

957 illegal_chars = set(revision).intersection(_revision_illegal_chars) 

958 if illegal_chars: 

959 raise RevisionError( 

960 "Character(s) '%s' not allowed in revision identifier '%s'" 

961 % (", ".join(sorted(illegal_chars)), revision) 

962 ) 

963 

964 def __init__( 

965 self, revision, down_revision, dependencies=None, branch_labels=None 

966 ): 

967 self.verify_rev_id(revision) 

968 self.revision = revision 

969 self.down_revision = tuple_rev_as_scalar(down_revision) 

970 self.dependencies = tuple_rev_as_scalar(dependencies) 

971 self._resolved_dependencies = () 

972 self._orig_branch_labels = util.to_tuple(branch_labels, default=()) 

973 self.branch_labels = set(self._orig_branch_labels) 

974 

975 def __repr__(self): 

976 args = [repr(self.revision), repr(self.down_revision)] 

977 if self.dependencies: 

978 args.append("dependencies=%r" % (self.dependencies,)) 

979 if self.branch_labels: 

980 args.append("branch_labels=%r" % (self.branch_labels,)) 

981 return "%s(%s)" % (self.__class__.__name__, ", ".join(args)) 

982 

983 def add_nextrev(self, revision): 

984 self._all_nextrev = self._all_nextrev.union([revision.revision]) 

985 if self.revision in revision._versioned_down_revisions: 

986 self.nextrev = self.nextrev.union([revision.revision]) 

987 

988 @property 

989 def _all_down_revisions(self): 

990 return ( 

991 util.to_tuple(self.down_revision, default=()) 

992 + self._resolved_dependencies 

993 ) 

994 

995 @property 

996 def _versioned_down_revisions(self): 

997 return util.to_tuple(self.down_revision, default=()) 

998 

999 @property 

1000 def is_head(self): 

1001 """Return True if this :class:`.Revision` is a 'head' revision. 

1002 

1003 This is determined based on whether any other :class:`.Script` 

1004 within the :class:`.ScriptDirectory` refers to this 

1005 :class:`.Script`. Multiple heads can be present. 

1006 

1007 """ 

1008 return not bool(self.nextrev) 

1009 

1010 @property 

1011 def _is_real_head(self): 

1012 return not bool(self._all_nextrev) 

1013 

1014 @property 

1015 def is_base(self): 

1016 """Return True if this :class:`.Revision` is a 'base' revision.""" 

1017 

1018 return self.down_revision is None 

1019 

1020 @property 

1021 def _is_real_base(self): 

1022 """Return True if this :class:`.Revision` is a "real" base revision, 

1023 e.g. that it has no dependencies either.""" 

1024 

1025 # we use self.dependencies here because this is called up 

1026 # in initialization where _real_dependencies isn't set up 

1027 # yet 

1028 return self.down_revision is None and self.dependencies is None 

1029 

1030 @property 

1031 def is_branch_point(self): 

1032 """Return True if this :class:`.Script` is a branch point. 

1033 

1034 A branchpoint is defined as a :class:`.Script` which is referred 

1035 to by more than one succeeding :class:`.Script`, that is more 

1036 than one :class:`.Script` has a `down_revision` identifier pointing 

1037 here. 

1038 

1039 """ 

1040 return len(self.nextrev) > 1 

1041 

1042 @property 

1043 def _is_real_branch_point(self): 

1044 """Return True if this :class:`.Script` is a 'real' branch point, 

1045 taking into account dependencies as well. 

1046 

1047 """ 

1048 return len(self._all_nextrev) > 1 

1049 

1050 @property 

1051 def is_merge_point(self): 

1052 """Return True if this :class:`.Script` is a merge point.""" 

1053 

1054 return len(self._versioned_down_revisions) > 1 

1055 

1056 

1057def tuple_rev_as_scalar(rev): 

1058 if not rev: 

1059 return None 

1060 elif len(rev) == 1: 

1061 return rev[0] 

1062 else: 

1063 return rev