| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721 |
- from __future__ import annotations
- import collections
- import re
- from typing import Any
- from typing import Callable
- from typing import cast
- from typing import Collection
- from typing import Deque
- from typing import Dict
- from typing import FrozenSet
- from typing import Iterable
- from typing import Iterator
- from typing import List
- from typing import Optional
- from typing import overload
- from typing import Protocol
- from typing import Sequence
- from typing import Set
- from typing import Tuple
- from typing import TYPE_CHECKING
- from typing import TypeVar
- from typing import Union
- from sqlalchemy import util as sqlautil
- from .. import util
- from ..util import not_none
- if TYPE_CHECKING:
- from typing import Literal
- _RevIdType = Union[str, List[str], Tuple[str, ...]]
- _GetRevArg = Union[
- str,
- Iterable[Optional[str]],
- Iterable[str],
- ]
- _RevisionIdentifierType = Union[str, Tuple[str, ...], None]
- _RevisionOrStr = Union["Revision", str]
- _RevisionOrBase = Union["Revision", "Literal['base']"]
- _InterimRevisionMapType = Dict[str, "Revision"]
- _RevisionMapType = Dict[Union[None, str, Tuple[()]], Optional["Revision"]]
- _T = TypeVar("_T")
- _TR = TypeVar("_TR", bound=Optional[_RevisionOrStr])
- _relative_destination = re.compile(r"(?:(.+?)@)?(\w+)?((?:\+|-)\d+)")
- _revision_illegal_chars = ["@", "-", "+"]
- class _CollectRevisionsProtocol(Protocol):
- def __call__(
- self,
- upper: _RevisionIdentifierType,
- lower: _RevisionIdentifierType,
- inclusive: bool,
- implicit_base: bool,
- assert_relative_length: bool,
- ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase], ...]]:
- ...
- class RevisionError(Exception):
- pass
- class RangeNotAncestorError(RevisionError):
- def __init__(
- self, lower: _RevisionIdentifierType, upper: _RevisionIdentifierType
- ) -> None:
- self.lower = lower
- self.upper = upper
- super().__init__(
- "Revision %s is not an ancestor of revision %s"
- % (lower or "base", upper or "base")
- )
- class MultipleHeads(RevisionError):
- def __init__(self, heads: Sequence[str], argument: Optional[str]) -> None:
- self.heads = heads
- self.argument = argument
- super().__init__(
- "Multiple heads are present for given argument '%s'; "
- "%s" % (argument, ", ".join(heads))
- )
- class ResolutionError(RevisionError):
- def __init__(self, message: str, argument: str) -> None:
- super().__init__(message)
- self.argument = argument
- class CycleDetected(RevisionError):
- kind = "Cycle"
- def __init__(self, revisions: Sequence[str]) -> None:
- self.revisions = revisions
- super().__init__(
- "%s is detected in revisions (%s)"
- % (self.kind, ", ".join(revisions))
- )
- class DependencyCycleDetected(CycleDetected):
- kind = "Dependency cycle"
- def __init__(self, revisions: Sequence[str]) -> None:
- super().__init__(revisions)
- class LoopDetected(CycleDetected):
- kind = "Self-loop"
- def __init__(self, revision: str) -> None:
- super().__init__([revision])
- class DependencyLoopDetected(DependencyCycleDetected, LoopDetected):
- kind = "Dependency self-loop"
- def __init__(self, revision: Sequence[str]) -> None:
- super().__init__(revision)
- class RevisionMap:
- """Maintains a map of :class:`.Revision` objects.
- :class:`.RevisionMap` is used by :class:`.ScriptDirectory` to maintain
- and traverse the collection of :class:`.Script` objects, which are
- themselves instances of :class:`.Revision`.
- """
- def __init__(self, generator: Callable[[], Iterable[Revision]]) -> None:
- """Construct a new :class:`.RevisionMap`.
- :param generator: a zero-arg callable that will generate an iterable
- of :class:`.Revision` instances to be used. These are typically
- :class:`.Script` subclasses within regular Alembic use.
- """
- self._generator = generator
- @util.memoized_property
- def heads(self) -> Tuple[str, ...]:
- """All "head" revisions as strings.
- This is normally a tuple of length one,
- unless unmerged branches are present.
- :return: a tuple of string revision numbers.
- """
- self._revision_map
- return self.heads
- @util.memoized_property
- def bases(self) -> Tuple[str, ...]:
- """All "base" revisions as strings.
- These are revisions that have a ``down_revision`` of None,
- or empty tuple.
- :return: a tuple of string revision numbers.
- """
- self._revision_map
- return self.bases
- @util.memoized_property
- def _real_heads(self) -> Tuple[str, ...]:
- """All "real" head revisions as strings.
- :return: a tuple of string revision numbers.
- """
- self._revision_map
- return self._real_heads
- @util.memoized_property
- def _real_bases(self) -> Tuple[str, ...]:
- """All "real" base revisions as strings.
- :return: a tuple of string revision numbers.
- """
- self._revision_map
- return self._real_bases
- @util.memoized_property
- def _revision_map(self) -> _RevisionMapType:
- """memoized attribute, initializes the revision map from the
- initial collection.
- """
- # Ordering required for some tests to pass (but not required in
- # general)
- map_: _InterimRevisionMapType = sqlautil.OrderedDict()
- heads: Set[Revision] = sqlautil.OrderedSet()
- _real_heads: Set[Revision] = sqlautil.OrderedSet()
- bases: Tuple[Revision, ...] = ()
- _real_bases: Tuple[Revision, ...] = ()
- has_branch_labels = set()
- all_revisions = set()
- for revision in self._generator():
- all_revisions.add(revision)
- if revision.revision in map_:
- util.warn(
- "Revision %s is present more than once" % revision.revision
- )
- map_[revision.revision] = revision
- if revision.branch_labels:
- has_branch_labels.add(revision)
- heads.add(revision)
- _real_heads.add(revision)
- if revision.is_base:
- bases += (revision,)
- if revision._is_real_base:
- _real_bases += (revision,)
- # add the branch_labels to the map_. We'll need these
- # to resolve the dependencies.
- rev_map = map_.copy()
- self._map_branch_labels(
- has_branch_labels, cast(_RevisionMapType, map_)
- )
- # resolve dependency names from branch labels and symbolic
- # names
- self._add_depends_on(all_revisions, cast(_RevisionMapType, map_))
- for rev in map_.values():
- for downrev in rev._all_down_revisions:
- if downrev not in map_:
- util.warn(
- "Revision %s referenced from %s is not present"
- % (downrev, rev)
- )
- down_revision = map_[downrev]
- down_revision.add_nextrev(rev)
- if downrev in rev._versioned_down_revisions:
- heads.discard(down_revision)
- _real_heads.discard(down_revision)
- # once the map has downrevisions populated, the dependencies
- # can be further refined to include only those which are not
- # already ancestors
- self._normalize_depends_on(all_revisions, cast(_RevisionMapType, map_))
- self._detect_cycles(rev_map, heads, bases, _real_heads, _real_bases)
- revision_map: _RevisionMapType = dict(map_.items())
- revision_map[None] = revision_map[()] = None
- self.heads = tuple(rev.revision for rev in heads)
- self._real_heads = tuple(rev.revision for rev in _real_heads)
- self.bases = tuple(rev.revision for rev in bases)
- self._real_bases = tuple(rev.revision for rev in _real_bases)
- self._add_branches(has_branch_labels, revision_map)
- return revision_map
- def _detect_cycles(
- self,
- rev_map: _InterimRevisionMapType,
- heads: Set[Revision],
- bases: Tuple[Revision, ...],
- _real_heads: Set[Revision],
- _real_bases: Tuple[Revision, ...],
- ) -> None:
- if not rev_map:
- return
- if not heads or not bases:
- raise CycleDetected(list(rev_map))
- total_space = {
- rev.revision
- for rev in self._iterate_related_revisions(
- lambda r: r._versioned_down_revisions,
- heads,
- map_=cast(_RevisionMapType, rev_map),
- )
- }.intersection(
- rev.revision
- for rev in self._iterate_related_revisions(
- lambda r: r.nextrev,
- bases,
- map_=cast(_RevisionMapType, rev_map),
- )
- )
- deleted_revs = set(rev_map.keys()) - total_space
- if deleted_revs:
- raise CycleDetected(sorted(deleted_revs))
- if not _real_heads or not _real_bases:
- raise DependencyCycleDetected(list(rev_map))
- total_space = {
- rev.revision
- for rev in self._iterate_related_revisions(
- lambda r: r._all_down_revisions,
- _real_heads,
- map_=cast(_RevisionMapType, rev_map),
- )
- }.intersection(
- rev.revision
- for rev in self._iterate_related_revisions(
- lambda r: r._all_nextrev,
- _real_bases,
- map_=cast(_RevisionMapType, rev_map),
- )
- )
- deleted_revs = set(rev_map.keys()) - total_space
- if deleted_revs:
- raise DependencyCycleDetected(sorted(deleted_revs))
- def _map_branch_labels(
- self, revisions: Collection[Revision], map_: _RevisionMapType
- ) -> None:
- for revision in revisions:
- if revision.branch_labels:
- assert revision._orig_branch_labels is not None
- for branch_label in revision._orig_branch_labels:
- if branch_label in map_:
- map_rev = map_[branch_label]
- assert map_rev is not None
- raise RevisionError(
- "Branch name '%s' in revision %s already "
- "used by revision %s"
- % (
- branch_label,
- revision.revision,
- map_rev.revision,
- )
- )
- map_[branch_label] = revision
- def _add_branches(
- self, revisions: Collection[Revision], map_: _RevisionMapType
- ) -> None:
- for revision in revisions:
- if revision.branch_labels:
- revision.branch_labels.update(revision.branch_labels)
- for node in self._get_descendant_nodes(
- [revision], map_, include_dependencies=False
- ):
- node.branch_labels.update(revision.branch_labels)
- parent = node
- while (
- parent
- and not parent._is_real_branch_point
- and not parent.is_merge_point
- ):
- parent.branch_labels.update(revision.branch_labels)
- if parent.down_revision:
- parent = map_[parent.down_revision]
- else:
- break
- def _add_depends_on(
- self, revisions: Collection[Revision], map_: _RevisionMapType
- ) -> None:
- """Resolve the 'dependencies' for each revision in a collection
- in terms of actual revision ids, as opposed to branch labels or other
- symbolic names.
- The collection is then assigned to the _resolved_dependencies
- attribute on each revision object.
- """
- for revision in revisions:
- if revision.dependencies:
- deps = [
- map_[dep] for dep in util.to_tuple(revision.dependencies)
- ]
- revision._resolved_dependencies = tuple(
- [d.revision for d in deps if d is not None]
- )
- else:
- revision._resolved_dependencies = ()
- def _normalize_depends_on(
- self, revisions: Collection[Revision], map_: _RevisionMapType
- ) -> None:
- """Create a collection of "dependencies" that omits dependencies
- that are already ancestor nodes for each revision in a given
- collection.
- This builds upon the _resolved_dependencies collection created in the
- _add_depends_on() method, looking in the fully populated revision map
- for ancestors, and omitting them as the _resolved_dependencies
- collection as it is copied to a new collection. The new collection is
- then assigned to the _normalized_resolved_dependencies attribute on
- each revision object.
- The collection is then used to determine the immediate "down revision"
- identifiers for this revision.
- """
- for revision in revisions:
- if revision._resolved_dependencies:
- normalized_resolved = set(revision._resolved_dependencies)
- for rev in self._get_ancestor_nodes(
- [revision],
- include_dependencies=False,
- map_=map_,
- ):
- if rev is revision:
- continue
- elif rev._resolved_dependencies:
- normalized_resolved.difference_update(
- rev._resolved_dependencies
- )
- revision._normalized_resolved_dependencies = tuple(
- normalized_resolved
- )
- else:
- revision._normalized_resolved_dependencies = ()
- def add_revision(self, revision: Revision, _replace: bool = False) -> None:
- """add a single revision to an existing map.
- This method is for single-revision use cases, it's not
- appropriate for fully populating an entire revision map.
- """
- map_ = self._revision_map
- if not _replace and revision.revision in map_:
- util.warn(
- "Revision %s is present more than once" % revision.revision
- )
- elif _replace and revision.revision not in map_:
- raise Exception("revision %s not in map" % revision.revision)
- map_[revision.revision] = revision
- revisions = [revision]
- self._add_branches(revisions, map_)
- self._map_branch_labels(revisions, map_)
- self._add_depends_on(revisions, map_)
- if revision.is_base:
- self.bases += (revision.revision,)
- if revision._is_real_base:
- self._real_bases += (revision.revision,)
- for downrev in revision._all_down_revisions:
- if downrev not in map_:
- util.warn(
- "Revision %s referenced from %s is not present"
- % (downrev, revision)
- )
- not_none(map_[downrev]).add_nextrev(revision)
- self._normalize_depends_on(revisions, map_)
- if revision._is_real_head:
- self._real_heads = tuple(
- head
- for head in self._real_heads
- if head
- not in set(revision._all_down_revisions).union(
- [revision.revision]
- )
- ) + (revision.revision,)
- if revision.is_head:
- self.heads = tuple(
- head
- for head in self.heads
- if head
- not in set(revision._versioned_down_revisions).union(
- [revision.revision]
- )
- ) + (revision.revision,)
- def get_current_head(
- self, branch_label: Optional[str] = None
- ) -> Optional[str]:
- """Return the current head revision.
- If the script directory has multiple heads
- due to branching, an error is raised;
- :meth:`.ScriptDirectory.get_heads` should be
- preferred.
- :param branch_label: optional branch name which will limit the
- heads considered to those which include that branch_label.
- :return: a string revision number.
- .. seealso::
- :meth:`.ScriptDirectory.get_heads`
- """
- current_heads: Sequence[str] = self.heads
- if branch_label:
- current_heads = self.filter_for_lineage(
- current_heads, branch_label
- )
- if len(current_heads) > 1:
- raise MultipleHeads(
- current_heads,
- "%s@head" % branch_label if branch_label else "head",
- )
- if current_heads:
- return current_heads[0]
- else:
- return None
- def _get_base_revisions(self, identifier: str) -> Tuple[str, ...]:
- return self.filter_for_lineage(self.bases, identifier)
- def get_revisions(
- self, id_: Optional[_GetRevArg]
- ) -> Tuple[Optional[_RevisionOrBase], ...]:
- """Return the :class:`.Revision` instances with the given rev id
- or identifiers.
- May be given a single identifier, a sequence of identifiers, or the
- special symbols "head" or "base". The result is a tuple of one
- or more identifiers, or an empty tuple in the case of "base".
- In the cases where 'head', 'heads' is requested and the
- revision map is empty, returns an empty tuple.
- Supports partial identifiers, where the given identifier
- is matched against all identifiers that start with the given
- characters; if there is exactly one match, that determines the
- full revision.
- """
- if isinstance(id_, (list, tuple, set, frozenset)):
- return sum([self.get_revisions(id_elem) for id_elem in id_], ())
- else:
- resolved_id, branch_label = self._resolve_revision_number(id_)
- if len(resolved_id) == 1:
- try:
- rint = int(resolved_id[0])
- if rint < 0:
- # branch@-n -> walk down from heads
- select_heads = self.get_revisions("heads")
- if branch_label is not None:
- select_heads = tuple(
- head
- for head in select_heads
- if branch_label
- in is_revision(head).branch_labels
- )
- return tuple(
- self._walk(head, steps=rint)
- for head in select_heads
- )
- except ValueError:
- # couldn't resolve as integer
- pass
- return tuple(
- self._revision_for_ident(rev_id, branch_label)
- for rev_id in resolved_id
- )
- def get_revision(self, id_: Optional[str]) -> Optional[Revision]:
- """Return the :class:`.Revision` instance with the given rev id.
- If a symbolic name such as "head" or "base" is given, resolves
- the identifier into the current head or base revision. If the symbolic
- name refers to multiples, :class:`.MultipleHeads` is raised.
- Supports partial identifiers, where the given identifier
- is matched against all identifiers that start with the given
- characters; if there is exactly one match, that determines the
- full revision.
- """
- resolved_id, branch_label = self._resolve_revision_number(id_)
- if len(resolved_id) > 1:
- raise MultipleHeads(resolved_id, id_)
- resolved: Union[str, Tuple[()]] = resolved_id[0] if resolved_id else ()
- return self._revision_for_ident(resolved, branch_label)
- def _resolve_branch(self, branch_label: str) -> Optional[Revision]:
- try:
- branch_rev = self._revision_map[branch_label]
- except KeyError:
- try:
- nonbranch_rev = self._revision_for_ident(branch_label)
- except ResolutionError as re:
- raise ResolutionError(
- "No such branch: '%s'" % branch_label, branch_label
- ) from re
- else:
- return nonbranch_rev
- else:
- return branch_rev
- def _revision_for_ident(
- self,
- resolved_id: Union[str, Tuple[()], None],
- check_branch: Optional[str] = None,
- ) -> Optional[Revision]:
- branch_rev: Optional[Revision]
- if check_branch:
- branch_rev = self._resolve_branch(check_branch)
- else:
- branch_rev = None
- revision: Union[Optional[Revision], Literal[False]]
- try:
- revision = self._revision_map[resolved_id]
- except KeyError:
- # break out to avoid misleading py3k stack traces
- revision = False
- revs: Sequence[str]
- if revision is False:
- assert resolved_id
- # do a partial lookup
- revs = [
- x
- for x in self._revision_map
- if x and len(x) > 3 and x.startswith(resolved_id)
- ]
- if branch_rev:
- revs = self.filter_for_lineage(revs, check_branch)
- if not revs:
- raise ResolutionError(
- "No such revision or branch '%s'%s"
- % (
- resolved_id,
- (
- "; please ensure at least four characters are "
- "present for partial revision identifier matches"
- if len(resolved_id) < 4
- else ""
- ),
- ),
- resolved_id,
- )
- elif len(revs) > 1:
- raise ResolutionError(
- "Multiple revisions start "
- "with '%s': %s..."
- % (resolved_id, ", ".join("'%s'" % r for r in revs[0:3])),
- resolved_id,
- )
- else:
- revision = self._revision_map[revs[0]]
- if check_branch and revision is not None:
- assert branch_rev is not None
- assert resolved_id
- if not self._shares_lineage(
- revision.revision, branch_rev.revision
- ):
- raise ResolutionError(
- "Revision %s is not a member of branch '%s'"
- % (revision.revision, check_branch),
- resolved_id,
- )
- return revision
- def _filter_into_branch_heads(
- self, targets: Iterable[Optional[_RevisionOrBase]]
- ) -> Set[Optional[_RevisionOrBase]]:
- targets = set(targets)
- for rev in list(targets):
- assert rev
- if targets.intersection(
- self._get_descendant_nodes([rev], include_dependencies=False)
- ).difference([rev]):
- targets.discard(rev)
- return targets
- def filter_for_lineage(
- self,
- targets: Iterable[_TR],
- check_against: Optional[str],
- include_dependencies: bool = False,
- ) -> Tuple[_TR, ...]:
- id_, branch_label = self._resolve_revision_number(check_against)
- shares = []
- if branch_label:
- shares.append(branch_label)
- if id_:
- shares.extend(id_)
- return tuple(
- tg
- for tg in targets
- if self._shares_lineage(
- tg, shares, include_dependencies=include_dependencies
- )
- )
- def _shares_lineage(
- self,
- target: Optional[_RevisionOrStr],
- test_against_revs: Sequence[_RevisionOrStr],
- include_dependencies: bool = False,
- ) -> bool:
- if not test_against_revs:
- return True
- if not isinstance(target, Revision):
- resolved_target = not_none(self._revision_for_ident(target))
- else:
- resolved_target = target
- resolved_test_against_revs = [
- self._revision_for_ident(test_against_rev)
- if not isinstance(test_against_rev, Revision)
- else test_against_rev
- for test_against_rev in util.to_tuple(
- test_against_revs, default=()
- )
- ]
- return bool(
- set(
- self._get_descendant_nodes(
- [resolved_target],
- include_dependencies=include_dependencies,
- )
- )
- .union(
- self._get_ancestor_nodes(
- [resolved_target],
- include_dependencies=include_dependencies,
- )
- )
- .intersection(resolved_test_against_revs)
- )
- def _resolve_revision_number(
- self, id_: Optional[_GetRevArg]
- ) -> Tuple[Tuple[str, ...], Optional[str]]:
- branch_label: Optional[str]
- if isinstance(id_, str) and "@" in id_:
- branch_label, id_ = id_.split("@", 1)
- elif id_ is not None and (
- (isinstance(id_, tuple) and id_ and not isinstance(id_[0], str))
- or not isinstance(id_, (str, tuple))
- ):
- raise RevisionError(
- "revision identifier %r is not a string; ensure database "
- "driver settings are correct" % (id_,)
- )
- else:
- branch_label = None
- # ensure map is loaded
- self._revision_map
- if id_ == "heads":
- if branch_label:
- return (
- self.filter_for_lineage(self.heads, branch_label),
- branch_label,
- )
- else:
- return self._real_heads, branch_label
- elif id_ == "head":
- current_head = self.get_current_head(branch_label)
- if current_head:
- return (current_head,), branch_label
- else:
- return (), branch_label
- elif id_ == "base" or id_ is None:
- return (), branch_label
- else:
- return util.to_tuple(id_, default=None), branch_label
- def iterate_revisions(
- self,
- upper: _RevisionIdentifierType,
- lower: _RevisionIdentifierType,
- implicit_base: bool = False,
- inclusive: bool = False,
- assert_relative_length: bool = True,
- select_for_downgrade: bool = False,
- ) -> Iterator[Revision]:
- """Iterate through script revisions, starting at the given
- upper revision identifier and ending at the lower.
- The traversal uses strictly the `down_revision`
- marker inside each migration script, so
- it is a requirement that upper >= lower,
- else you'll get nothing back.
- The iterator yields :class:`.Revision` objects.
- """
- fn: _CollectRevisionsProtocol
- if select_for_downgrade:
- fn = self._collect_downgrade_revisions
- else:
- fn = self._collect_upgrade_revisions
- revisions, heads = fn(
- upper,
- lower,
- inclusive=inclusive,
- implicit_base=implicit_base,
- assert_relative_length=assert_relative_length,
- )
- for node in self._topological_sort(revisions, heads):
- yield not_none(self.get_revision(node))
- def _get_descendant_nodes(
- self,
- targets: Collection[Optional[_RevisionOrBase]],
- map_: Optional[_RevisionMapType] = None,
- check: bool = False,
- omit_immediate_dependencies: bool = False,
- include_dependencies: bool = True,
- ) -> Iterator[Any]:
- if omit_immediate_dependencies:
- def fn(rev: Revision) -> Iterable[str]:
- if rev not in targets:
- return rev._all_nextrev
- else:
- return rev.nextrev
- elif include_dependencies:
- def fn(rev: Revision) -> Iterable[str]:
- return rev._all_nextrev
- else:
- def fn(rev: Revision) -> Iterable[str]:
- return rev.nextrev
- return self._iterate_related_revisions(
- fn, targets, map_=map_, check=check
- )
- def _get_ancestor_nodes(
- self,
- targets: Collection[Optional[_RevisionOrBase]],
- map_: Optional[_RevisionMapType] = None,
- check: bool = False,
- include_dependencies: bool = True,
- ) -> Iterator[Revision]:
- if include_dependencies:
- def fn(rev: Revision) -> Iterable[str]:
- return rev._normalized_down_revisions
- else:
- def fn(rev: Revision) -> Iterable[str]:
- return rev._versioned_down_revisions
- return self._iterate_related_revisions(
- fn, targets, map_=map_, check=check
- )
- def _iterate_related_revisions(
- self,
- fn: Callable[[Revision], Iterable[str]],
- targets: Collection[Optional[_RevisionOrBase]],
- map_: Optional[_RevisionMapType],
- check: bool = False,
- ) -> Iterator[Revision]:
- if map_ is None:
- map_ = self._revision_map
- seen = set()
- todo: Deque[Revision] = collections.deque()
- for target_for in targets:
- target = is_revision(target_for)
- todo.append(target)
- if check:
- per_target = set()
- while todo:
- rev = todo.pop()
- if check:
- per_target.add(rev)
- if rev in seen:
- continue
- seen.add(rev)
- # Check for map errors before collecting.
- for rev_id in fn(rev):
- next_rev = map_[rev_id]
- assert next_rev is not None
- if next_rev.revision != rev_id:
- raise RevisionError(
- "Dependency resolution failed; broken map"
- )
- todo.append(next_rev)
- yield rev
- if check:
- overlaps = per_target.intersection(targets).difference(
- [target]
- )
- if overlaps:
- raise RevisionError(
- "Requested revision %s overlaps with "
- "other requested revisions %s"
- % (
- target.revision,
- ", ".join(r.revision for r in overlaps),
- )
- )
- def _topological_sort(
- self,
- revisions: Collection[Revision],
- heads: Any,
- ) -> List[str]:
- """Yield revision ids of a collection of Revision objects in
- topological sorted order (i.e. revisions always come after their
- down_revisions and dependencies). Uses the order of keys in
- _revision_map to sort.
- """
- id_to_rev = self._revision_map
- def get_ancestors(rev_id: str) -> Set[str]:
- return {
- r.revision
- for r in self._get_ancestor_nodes([id_to_rev[rev_id]])
- }
- todo = {d.revision for d in revisions}
- # Use revision map (ordered dict) key order to pre-sort.
- inserted_order = list(self._revision_map)
- current_heads = list(
- sorted(
- {d.revision for d in heads if d.revision in todo},
- key=inserted_order.index,
- )
- )
- ancestors_by_idx = [get_ancestors(rev_id) for rev_id in current_heads]
- output = []
- current_candidate_idx = 0
- while current_heads:
- candidate = current_heads[current_candidate_idx]
- for check_head_index, ancestors in enumerate(ancestors_by_idx):
- # scan all the heads. see if we can continue walking
- # down the current branch indicated by current_candidate_idx.
- if (
- check_head_index != current_candidate_idx
- and candidate in ancestors
- ):
- current_candidate_idx = check_head_index
- # nope, another head is dependent on us, they have
- # to be traversed first
- break
- else:
- # yup, we can emit
- if candidate in todo:
- output.append(candidate)
- todo.remove(candidate)
- # now update the heads with our ancestors.
- candidate_rev = id_to_rev[candidate]
- assert candidate_rev is not None
- heads_to_add = [
- r
- for r in candidate_rev._normalized_down_revisions
- if r in todo and r not in current_heads
- ]
- if not heads_to_add:
- # no ancestors, so remove this head from the list
- del current_heads[current_candidate_idx]
- del ancestors_by_idx[current_candidate_idx]
- current_candidate_idx = max(current_candidate_idx - 1, 0)
- else:
- if (
- not candidate_rev._normalized_resolved_dependencies
- and len(candidate_rev._versioned_down_revisions) == 1
- ):
- current_heads[current_candidate_idx] = heads_to_add[0]
- # for plain movement down a revision line without
- # any mergepoints, branchpoints, or deps, we
- # can update the ancestors collection directly
- # by popping out the candidate we just emitted
- ancestors_by_idx[current_candidate_idx].discard(
- candidate
- )
- else:
- # otherwise recalculate it again, things get
- # complicated otherwise. This can possibly be
- # improved to not run the whole ancestor thing
- # each time but it was getting complicated
- current_heads[current_candidate_idx] = heads_to_add[0]
- current_heads.extend(heads_to_add[1:])
- ancestors_by_idx[
- current_candidate_idx
- ] = get_ancestors(heads_to_add[0])
- ancestors_by_idx.extend(
- get_ancestors(head) for head in heads_to_add[1:]
- )
- assert not todo
- return output
- def _walk(
- self,
- start: Optional[Union[str, Revision]],
- steps: int,
- branch_label: Optional[str] = None,
- no_overwalk: bool = True,
- ) -> Optional[_RevisionOrBase]:
- """
- Walk the requested number of :steps up (steps > 0) or down (steps < 0)
- the revision tree.
- :branch_label is used to select branches only when walking up.
- If the walk goes past the boundaries of the tree and :no_overwalk is
- True, None is returned, otherwise the walk terminates early.
- A RevisionError is raised if there is no unambiguous revision to
- walk to.
- """
- initial: Optional[_RevisionOrBase]
- if isinstance(start, str):
- initial = self.get_revision(start)
- else:
- initial = start
- children: Sequence[Optional[_RevisionOrBase]]
- for _ in range(abs(steps)):
- if steps > 0:
- assert initial != "base" # type: ignore[comparison-overlap]
- # Walk up
- walk_up = [
- is_revision(rev)
- for rev in self.get_revisions(
- self.bases if initial is None else initial.nextrev
- )
- ]
- if branch_label:
- children = self.filter_for_lineage(walk_up, branch_label)
- else:
- children = walk_up
- else:
- # Walk down
- if initial == "base": # type: ignore[comparison-overlap]
- children = ()
- else:
- children = self.get_revisions(
- self.heads
- if initial is None
- else initial.down_revision
- )
- if not children:
- children = ("base",)
- if not children:
- # This will return an invalid result if no_overwalk, otherwise
- # further steps will stay where we are.
- ret = None if no_overwalk else initial
- return ret
- elif len(children) > 1:
- raise RevisionError("Ambiguous walk")
- initial = children[0]
- return initial
- def _parse_downgrade_target(
- self,
- current_revisions: _RevisionIdentifierType,
- target: _RevisionIdentifierType,
- assert_relative_length: bool,
- ) -> Tuple[Optional[str], Optional[_RevisionOrBase]]:
- """
- Parse downgrade command syntax :target to retrieve the target revision
- and branch label (if any) given the :current_revisions stamp of the
- database.
- Returns a tuple (branch_label, target_revision) where branch_label
- is a string from the command specifying the branch to consider (or
- None if no branch given), and target_revision is a Revision object
- which the command refers to. target_revisions is None if the command
- refers to 'base'. The target may be specified in absolute form, or
- relative to :current_revisions.
- """
- if target is None:
- return None, None
- assert isinstance(
- target, str
- ), "Expected downgrade target in string form"
- match = _relative_destination.match(target)
- if match:
- branch_label, symbol, relative = match.groups()
- rel_int = int(relative)
- if rel_int >= 0:
- if symbol is None:
- # Downgrading to current + n is not valid.
- raise RevisionError(
- "Relative revision %s didn't "
- "produce %d migrations" % (relative, abs(rel_int))
- )
- # Find target revision relative to given symbol.
- rev = self._walk(
- symbol,
- rel_int,
- branch_label,
- no_overwalk=assert_relative_length,
- )
- if rev is None:
- raise RevisionError("Walked too far")
- return branch_label, rev
- else:
- relative_revision = symbol is None
- if relative_revision:
- # Find target revision relative to current state.
- if branch_label:
- cr_tuple = util.to_tuple(current_revisions)
- symbol_list: Sequence[str]
- symbol_list = self.filter_for_lineage(
- cr_tuple, branch_label
- )
- if not symbol_list:
- # check the case where there are multiple branches
- # but there is currently a single heads, since all
- # other branch heads are dependent of the current
- # single heads.
- all_current = cast(
- Set[Revision], self._get_all_current(cr_tuple)
- )
- sl_all_current = self.filter_for_lineage(
- all_current, branch_label
- )
- symbol_list = [
- r.revision if r else r # type: ignore[misc]
- for r in sl_all_current
- ]
- assert len(symbol_list) == 1
- symbol = symbol_list[0]
- else:
- current_revisions = util.to_tuple(current_revisions)
- if not current_revisions:
- raise RevisionError(
- "Relative revision %s didn't "
- "produce %d migrations"
- % (relative, abs(rel_int))
- )
- # Have to check uniques here for duplicate rows test.
- if len(set(current_revisions)) > 1:
- util.warn(
- "downgrade -1 from multiple heads is "
- "ambiguous; "
- "this usage will be disallowed in a future "
- "release."
- )
- symbol = current_revisions[0]
- # Restrict iteration to just the selected branch when
- # ambiguous branches are involved.
- branch_label = symbol
- # Walk down the tree to find downgrade target.
- rev = self._walk(
- start=self.get_revision(symbol)
- if branch_label is None
- else self.get_revision("%s@%s" % (branch_label, symbol)),
- steps=rel_int,
- no_overwalk=assert_relative_length,
- )
- if rev is None:
- if relative_revision:
- raise RevisionError(
- "Relative revision %s didn't "
- "produce %d migrations" % (relative, abs(rel_int))
- )
- else:
- raise RevisionError("Walked too far")
- return branch_label, rev
- # No relative destination given, revision specified is absolute.
- branch_label, _, symbol = target.rpartition("@")
- if not branch_label:
- branch_label = None
- return branch_label, self.get_revision(symbol)
- def _parse_upgrade_target(
- self,
- current_revisions: _RevisionIdentifierType,
- target: _RevisionIdentifierType,
- assert_relative_length: bool,
- ) -> Tuple[Optional[_RevisionOrBase], ...]:
- """
- Parse upgrade command syntax :target to retrieve the target revision
- and given the :current_revisions stamp of the database.
- Returns a tuple of Revision objects which should be iterated/upgraded
- to. The target may be specified in absolute form, or relative to
- :current_revisions.
- """
- if isinstance(target, str):
- match = _relative_destination.match(target)
- else:
- match = None
- if not match:
- # No relative destination, target is absolute.
- return self.get_revisions(target)
- current_revisions_tup: Union[str, Tuple[Optional[str], ...], None]
- current_revisions_tup = util.to_tuple(current_revisions)
- branch_label, symbol, relative_str = match.groups()
- relative = int(relative_str)
- if relative > 0:
- if symbol is None:
- if not current_revisions_tup:
- current_revisions_tup = (None,)
- # Try to filter to a single target (avoid ambiguous branches).
- start_revs = current_revisions_tup
- if branch_label:
- start_revs = self.filter_for_lineage(
- self.get_revisions(current_revisions_tup), # type: ignore[arg-type] # noqa: E501
- branch_label,
- )
- if not start_revs:
- # The requested branch is not a head, so we need to
- # backtrack to find a branchpoint.
- active_on_branch = self.filter_for_lineage(
- self._get_ancestor_nodes(
- self.get_revisions(current_revisions_tup)
- ),
- branch_label,
- )
- # Find the tips of this set of revisions (revisions
- # without children within the set).
- start_revs = tuple(
- {rev.revision for rev in active_on_branch}
- - {
- down
- for rev in active_on_branch
- for down in rev._normalized_down_revisions
- }
- )
- if not start_revs:
- # We must need to go right back to base to find
- # a starting point for this branch.
- start_revs = (None,)
- if len(start_revs) > 1:
- raise RevisionError(
- "Ambiguous upgrade from multiple current revisions"
- )
- # Walk up from unique target revision.
- rev = self._walk(
- start=start_revs[0],
- steps=relative,
- branch_label=branch_label,
- no_overwalk=assert_relative_length,
- )
- if rev is None:
- raise RevisionError(
- "Relative revision %s didn't "
- "produce %d migrations" % (relative_str, abs(relative))
- )
- return (rev,)
- else:
- # Walk is relative to a given revision, not the current state.
- return (
- self._walk(
- start=self.get_revision(symbol),
- steps=relative,
- branch_label=branch_label,
- no_overwalk=assert_relative_length,
- ),
- )
- else:
- if symbol is None:
- # Upgrading to current - n is not valid.
- raise RevisionError(
- "Relative revision %s didn't "
- "produce %d migrations" % (relative, abs(relative))
- )
- return (
- self._walk(
- start=self.get_revision(symbol)
- if branch_label is None
- else self.get_revision("%s@%s" % (branch_label, symbol)),
- steps=relative,
- no_overwalk=assert_relative_length,
- ),
- )
- def _collect_downgrade_revisions(
- self,
- upper: _RevisionIdentifierType,
- lower: _RevisionIdentifierType,
- inclusive: bool,
- implicit_base: bool,
- assert_relative_length: bool,
- ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase], ...]]:
- """
- Compute the set of current revisions specified by :upper, and the
- downgrade target specified by :target. Return all dependents of target
- which are currently active.
- :inclusive=True includes the target revision in the set
- """
- branch_label, target_revision = self._parse_downgrade_target(
- current_revisions=upper,
- target=lower,
- assert_relative_length=assert_relative_length,
- )
- if target_revision == "base":
- target_revision = None
- assert target_revision is None or isinstance(target_revision, Revision)
- roots: List[Revision]
- # Find candidates to drop.
- if target_revision is None:
- # Downgrading back to base: find all tree roots.
- roots = [
- rev
- for rev in self._revision_map.values()
- if rev is not None and rev.down_revision is None
- ]
- elif inclusive:
- # inclusive implies target revision should also be dropped
- roots = [target_revision]
- else:
- # Downgrading to fixed target: find all direct children.
- roots = [
- is_revision(rev)
- for rev in self.get_revisions(target_revision.nextrev)
- ]
- if branch_label and len(roots) > 1:
- # Need to filter roots.
- ancestors = {
- rev.revision
- for rev in self._get_ancestor_nodes(
- [self._resolve_branch(branch_label)],
- include_dependencies=False,
- )
- }
- # Intersection gives the root revisions we are trying to
- # rollback with the downgrade.
- roots = [
- is_revision(rev)
- for rev in self.get_revisions(
- {rev.revision for rev in roots}.intersection(ancestors)
- )
- ]
- # Ensure we didn't throw everything away when filtering branches.
- if len(roots) == 0:
- raise RevisionError(
- "Not a valid downgrade target from current heads"
- )
- heads = self.get_revisions(upper)
- # Aim is to drop :branch_revision; to do so we also need to drop its
- # descendents and anything dependent on it.
- downgrade_revisions = set(
- self._get_descendant_nodes(
- roots,
- include_dependencies=True,
- omit_immediate_dependencies=False,
- )
- )
- active_revisions = set(
- self._get_ancestor_nodes(heads, include_dependencies=True)
- )
- # Emit revisions to drop in reverse topological sorted order.
- downgrade_revisions.intersection_update(active_revisions)
- if implicit_base:
- # Wind other branches back to base.
- downgrade_revisions.update(
- active_revisions.difference(self._get_ancestor_nodes(roots))
- )
- if (
- target_revision is not None
- and not downgrade_revisions
- and target_revision not in heads
- ):
- # Empty intersection: target revs are not present.
- raise RangeNotAncestorError("Nothing to drop", upper)
- return downgrade_revisions, heads
- def _collect_upgrade_revisions(
- self,
- upper: _RevisionIdentifierType,
- lower: _RevisionIdentifierType,
- inclusive: bool,
- implicit_base: bool,
- assert_relative_length: bool,
- ) -> Tuple[Set[Revision], Tuple[Revision, ...]]:
- """
- Compute the set of required revisions specified by :upper, and the
- current set of active revisions specified by :lower. Find the
- difference between the two to compute the required upgrades.
- :inclusive=True includes the current/lower revisions in the set
- :implicit_base=False only returns revisions which are downstream
- of the current/lower revisions. Dependencies from branches with
- different bases will not be included.
- """
- targets: Collection[Revision] = [
- is_revision(rev)
- for rev in self._parse_upgrade_target(
- current_revisions=lower,
- target=upper,
- assert_relative_length=assert_relative_length,
- )
- ]
- # assert type(targets) is tuple, "targets should be a tuple"
- # Handled named bases (e.g. branch@... -> heads should only produce
- # targets on the given branch)
- if isinstance(lower, str) and "@" in lower:
- branch, _, _ = lower.partition("@")
- branch_rev = self.get_revision(branch)
- if branch_rev is not None and branch_rev.revision == branch:
- # A revision was used as a label; get its branch instead
- assert len(branch_rev.branch_labels) == 1
- branch = next(iter(branch_rev.branch_labels))
- targets = {
- need for need in targets if branch in need.branch_labels
- }
- required_node_set = set(
- self._get_ancestor_nodes(
- targets, check=True, include_dependencies=True
- )
- ).union(targets)
- current_revisions = self.get_revisions(lower)
- if not implicit_base and any(
- rev not in required_node_set
- for rev in current_revisions
- if rev is not None
- ):
- raise RangeNotAncestorError(lower, upper)
- assert (
- type(current_revisions) is tuple
- ), "current_revisions should be a tuple"
- # Special case where lower = a relative value (get_revisions can't
- # find it)
- if current_revisions and current_revisions[0] is None:
- _, rev = self._parse_downgrade_target(
- current_revisions=upper,
- target=lower,
- assert_relative_length=assert_relative_length,
- )
- assert rev
- if rev == "base":
- current_revisions = tuple()
- lower = None
- else:
- current_revisions = (rev,)
- lower = rev.revision
- current_node_set = set(
- self._get_ancestor_nodes(
- current_revisions, check=True, include_dependencies=True
- )
- ).union(current_revisions)
- needs = required_node_set.difference(current_node_set)
- # Include the lower revision (=current_revisions?) in the iteration
- if inclusive:
- needs.update(is_revision(rev) for rev in self.get_revisions(lower))
- # By default, base is implicit as we want all dependencies returned.
- # Base is also implicit if lower = base
- # implicit_base=False -> only return direct downstreams of
- # current_revisions
- if current_revisions and not implicit_base:
- lower_descendents = self._get_descendant_nodes(
- [is_revision(rev) for rev in current_revisions],
- check=True,
- include_dependencies=False,
- )
- needs.intersection_update(lower_descendents)
- return needs, tuple(targets)
- def _get_all_current(
- self, id_: Tuple[str, ...]
- ) -> Set[Optional[_RevisionOrBase]]:
- top_revs: Set[Optional[_RevisionOrBase]]
- top_revs = set(self.get_revisions(id_))
- top_revs.update(
- self._get_ancestor_nodes(list(top_revs), include_dependencies=True)
- )
- return self._filter_into_branch_heads(top_revs)
- class Revision:
- """Base class for revisioned objects.
- The :class:`.Revision` class is the base of the more public-facing
- :class:`.Script` object, which represents a migration script.
- The mechanics of revision management and traversal are encapsulated
- within :class:`.Revision`, while :class:`.Script` applies this logic
- to Python files in a version directory.
- """
- nextrev: FrozenSet[str] = frozenset()
- """following revisions, based on down_revision only."""
- _all_nextrev: FrozenSet[str] = frozenset()
- revision: str = None # type: ignore[assignment]
- """The string revision number."""
- down_revision: Optional[_RevIdType] = None
- """The ``down_revision`` identifier(s) within the migration script.
- Note that the total set of "down" revisions is
- down_revision + dependencies.
- """
- dependencies: Optional[_RevIdType] = None
- """Additional revisions which this revision is dependent on.
- From a migration standpoint, these dependencies are added to the
- down_revision to form the full iteration. However, the separation
- of down_revision from "dependencies" is to assist in navigating
- a history that contains many branches, typically a multi-root scenario.
- """
- branch_labels: Set[str] = None # type: ignore[assignment]
- """Optional string/tuple of symbolic names to apply to this
- revision's branch"""
- _resolved_dependencies: Tuple[str, ...]
- _normalized_resolved_dependencies: Tuple[str, ...]
- @classmethod
- def verify_rev_id(cls, revision: str) -> None:
- illegal_chars = set(revision).intersection(_revision_illegal_chars)
- if illegal_chars:
- raise RevisionError(
- "Character(s) '%s' not allowed in revision identifier '%s'"
- % (", ".join(sorted(illegal_chars)), revision)
- )
- def __init__(
- self,
- revision: str,
- down_revision: Optional[Union[str, Tuple[str, ...]]],
- dependencies: Optional[Union[str, Tuple[str, ...]]] = None,
- branch_labels: Optional[Union[str, Tuple[str, ...]]] = None,
- ) -> None:
- if down_revision and revision in util.to_tuple(down_revision):
- raise LoopDetected(revision)
- elif dependencies is not None and revision in util.to_tuple(
- dependencies
- ):
- raise DependencyLoopDetected(revision)
- self.verify_rev_id(revision)
- self.revision = revision
- self.down_revision = tuple_rev_as_scalar(util.to_tuple(down_revision))
- self.dependencies = tuple_rev_as_scalar(util.to_tuple(dependencies))
- self._orig_branch_labels = util.to_tuple(branch_labels, default=())
- self.branch_labels = set(self._orig_branch_labels)
- def __repr__(self) -> str:
- args = [repr(self.revision), repr(self.down_revision)]
- if self.dependencies:
- args.append("dependencies=%r" % (self.dependencies,))
- if self.branch_labels:
- args.append("branch_labels=%r" % (self.branch_labels,))
- return "%s(%s)" % (self.__class__.__name__, ", ".join(args))
- def add_nextrev(self, revision: Revision) -> None:
- self._all_nextrev = self._all_nextrev.union([revision.revision])
- if self.revision in revision._versioned_down_revisions:
- self.nextrev = self.nextrev.union([revision.revision])
- @property
- def _all_down_revisions(self) -> Tuple[str, ...]:
- return util.dedupe_tuple(
- util.to_tuple(self.down_revision, default=())
- + self._resolved_dependencies
- )
- @property
- def _normalized_down_revisions(self) -> Tuple[str, ...]:
- """return immediate down revisions for a rev, omitting dependencies
- that are still dependencies of ancestors.
- """
- return util.dedupe_tuple(
- util.to_tuple(self.down_revision, default=())
- + self._normalized_resolved_dependencies
- )
- @property
- def _versioned_down_revisions(self) -> Tuple[str, ...]:
- return util.to_tuple(self.down_revision, default=())
- @property
- def is_head(self) -> bool:
- """Return True if this :class:`.Revision` is a 'head' revision.
- This is determined based on whether any other :class:`.Script`
- within the :class:`.ScriptDirectory` refers to this
- :class:`.Script`. Multiple heads can be present.
- """
- return not bool(self.nextrev)
- @property
- def _is_real_head(self) -> bool:
- return not bool(self._all_nextrev)
- @property
- def is_base(self) -> bool:
- """Return True if this :class:`.Revision` is a 'base' revision."""
- return self.down_revision is None
- @property
- def _is_real_base(self) -> bool:
- """Return True if this :class:`.Revision` is a "real" base revision,
- e.g. that it has no dependencies either."""
- # we use self.dependencies here because this is called up
- # in initialization where _real_dependencies isn't set up
- # yet
- return self.down_revision is None and self.dependencies is None
- @property
- def is_branch_point(self) -> bool:
- """Return True if this :class:`.Script` is a branch point.
- A branchpoint is defined as a :class:`.Script` which is referred
- to by more than one succeeding :class:`.Script`, that is more
- than one :class:`.Script` has a `down_revision` identifier pointing
- here.
- """
- return len(self.nextrev) > 1
- @property
- def _is_real_branch_point(self) -> bool:
- """Return True if this :class:`.Script` is a 'real' branch point,
- taking into account dependencies as well.
- """
- return len(self._all_nextrev) > 1
- @property
- def is_merge_point(self) -> bool:
- """Return True if this :class:`.Script` is a merge point."""
- return len(self._versioned_down_revisions) > 1
- @overload
- def tuple_rev_as_scalar(rev: None) -> None:
- ...
- @overload
- def tuple_rev_as_scalar(
- rev: Union[Tuple[_T, ...], List[_T]]
- ) -> Union[_T, Tuple[_T, ...], List[_T]]:
- ...
- def tuple_rev_as_scalar(
- rev: Optional[Sequence[_T]],
- ) -> Union[_T, Sequence[_T], None]:
- if not rev:
- return None
- elif len(rev) == 1:
- return rev[0]
- else:
- return rev
- def is_revision(rev: Any) -> Revision:
- assert isinstance(rev, Revision)
- return rev
|