base.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. from __future__ import annotations
  2. from contextlib import contextmanager
  3. import datetime
  4. import os
  5. import re
  6. import shutil
  7. import sys
  8. from types import ModuleType
  9. from typing import Any
  10. from typing import cast
  11. from typing import Iterator
  12. from typing import List
  13. from typing import Mapping
  14. from typing import Optional
  15. from typing import Sequence
  16. from typing import Set
  17. from typing import Tuple
  18. from typing import TYPE_CHECKING
  19. from typing import Union
  20. from . import revision
  21. from . import write_hooks
  22. from .. import util
  23. from ..runtime import migration
  24. from ..util import compat
  25. from ..util import not_none
  26. if TYPE_CHECKING:
  27. from .revision import _GetRevArg
  28. from .revision import _RevIdType
  29. from .revision import Revision
  30. from ..config import Config
  31. from ..config import MessagingOptions
  32. from ..runtime.migration import RevisionStep
  33. from ..runtime.migration import StampStep
  34. try:
  35. if compat.py39:
  36. from zoneinfo import ZoneInfo
  37. from zoneinfo import ZoneInfoNotFoundError
  38. else:
  39. from backports.zoneinfo import ZoneInfo # type: ignore[import-not-found,no-redef] # noqa: E501
  40. from backports.zoneinfo import ZoneInfoNotFoundError # type: ignore[no-redef] # noqa: E501
  41. except ImportError:
  42. ZoneInfo = None # type: ignore[assignment, misc]
  43. _sourceless_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)(c|o)?$")
  44. _only_source_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)$")
  45. _legacy_rev = re.compile(r"([a-f0-9]+)\.py$")
  46. _slug_re = re.compile(r"\w+")
  47. _default_file_template = "%(rev)s_%(slug)s"
  48. _split_on_space_comma = re.compile(r", *|(?: +)")
  49. _split_on_space_comma_colon = re.compile(r", *|(?: +)|\:")
  50. class ScriptDirectory:
  51. """Provides operations upon an Alembic script directory.
  52. This object is useful to get information as to current revisions,
  53. most notably being able to get at the "head" revision, for schemes
  54. that want to test if the current revision in the database is the most
  55. recent::
  56. from alembic.script import ScriptDirectory
  57. from alembic.config import Config
  58. config = Config()
  59. config.set_main_option("script_location", "myapp:migrations")
  60. script = ScriptDirectory.from_config(config)
  61. head_revision = script.get_current_head()
  62. """
  63. def __init__(
  64. self,
  65. dir: str, # noqa
  66. file_template: str = _default_file_template,
  67. truncate_slug_length: Optional[int] = 40,
  68. version_locations: Optional[List[str]] = None,
  69. sourceless: bool = False,
  70. output_encoding: str = "utf-8",
  71. timezone: Optional[str] = None,
  72. hook_config: Optional[Mapping[str, str]] = None,
  73. recursive_version_locations: bool = False,
  74. messaging_opts: MessagingOptions = cast(
  75. "MessagingOptions", util.EMPTY_DICT
  76. ),
  77. ) -> None:
  78. self.dir = dir
  79. self.file_template = file_template
  80. self.version_locations = version_locations
  81. self.truncate_slug_length = truncate_slug_length or 40
  82. self.sourceless = sourceless
  83. self.output_encoding = output_encoding
  84. self.revision_map = revision.RevisionMap(self._load_revisions)
  85. self.timezone = timezone
  86. self.hook_config = hook_config
  87. self.recursive_version_locations = recursive_version_locations
  88. self.messaging_opts = messaging_opts
  89. if not os.access(dir, os.F_OK):
  90. raise util.CommandError(
  91. "Path doesn't exist: %r. Please use "
  92. "the 'init' command to create a new "
  93. "scripts folder." % os.path.abspath(dir)
  94. )
  95. @property
  96. def versions(self) -> str:
  97. loc = self._version_locations
  98. if len(loc) > 1:
  99. raise util.CommandError("Multiple version_locations present")
  100. else:
  101. return loc[0]
  102. @util.memoized_property
  103. def _version_locations(self) -> Sequence[str]:
  104. if self.version_locations:
  105. return [
  106. os.path.abspath(util.coerce_resource_to_filename(location))
  107. for location in self.version_locations
  108. ]
  109. else:
  110. return (os.path.abspath(os.path.join(self.dir, "versions")),)
  111. def _load_revisions(self) -> Iterator[Script]:
  112. if self.version_locations:
  113. paths = [
  114. vers
  115. for vers in self._version_locations
  116. if os.path.exists(vers)
  117. ]
  118. else:
  119. paths = [self.versions]
  120. dupes = set()
  121. for vers in paths:
  122. for file_path in Script._list_py_dir(self, vers):
  123. real_path = os.path.realpath(file_path)
  124. if real_path in dupes:
  125. util.warn(
  126. "File %s loaded twice! ignoring. Please ensure "
  127. "version_locations is unique." % real_path
  128. )
  129. continue
  130. dupes.add(real_path)
  131. filename = os.path.basename(real_path)
  132. dir_name = os.path.dirname(real_path)
  133. script = Script._from_filename(self, dir_name, filename)
  134. if script is None:
  135. continue
  136. yield script
  137. @classmethod
  138. def from_config(cls, config: Config) -> ScriptDirectory:
  139. """Produce a new :class:`.ScriptDirectory` given a :class:`.Config`
  140. instance.
  141. The :class:`.Config` need only have the ``script_location`` key
  142. present.
  143. """
  144. script_location = config.get_main_option("script_location")
  145. if script_location is None:
  146. raise util.CommandError(
  147. "No 'script_location' key " "found in configuration."
  148. )
  149. truncate_slug_length: Optional[int]
  150. tsl = config.get_main_option("truncate_slug_length")
  151. if tsl is not None:
  152. truncate_slug_length = int(tsl)
  153. else:
  154. truncate_slug_length = None
  155. version_locations_str = config.get_main_option("version_locations")
  156. version_locations: Optional[List[str]]
  157. if version_locations_str:
  158. version_path_separator = config.get_main_option(
  159. "version_path_separator"
  160. )
  161. split_on_path = {
  162. None: None,
  163. "space": " ",
  164. "os": os.pathsep,
  165. ":": ":",
  166. ";": ";",
  167. }
  168. try:
  169. split_char: Optional[str] = split_on_path[
  170. version_path_separator
  171. ]
  172. except KeyError as ke:
  173. raise ValueError(
  174. "'%s' is not a valid value for "
  175. "version_path_separator; "
  176. "expected 'space', 'os', ':', ';'" % version_path_separator
  177. ) from ke
  178. else:
  179. if split_char is None:
  180. # legacy behaviour for backwards compatibility
  181. version_locations = _split_on_space_comma.split(
  182. version_locations_str
  183. )
  184. else:
  185. version_locations = [
  186. x for x in version_locations_str.split(split_char) if x
  187. ]
  188. else:
  189. version_locations = None
  190. prepend_sys_path = config.get_main_option("prepend_sys_path")
  191. if prepend_sys_path:
  192. sys.path[:0] = list(
  193. _split_on_space_comma_colon.split(prepend_sys_path)
  194. )
  195. rvl = config.get_main_option("recursive_version_locations") == "true"
  196. return ScriptDirectory(
  197. util.coerce_resource_to_filename(script_location),
  198. file_template=config.get_main_option(
  199. "file_template", _default_file_template
  200. ),
  201. truncate_slug_length=truncate_slug_length,
  202. sourceless=config.get_main_option("sourceless") == "true",
  203. output_encoding=config.get_main_option("output_encoding", "utf-8"),
  204. version_locations=version_locations,
  205. timezone=config.get_main_option("timezone"),
  206. hook_config=config.get_section("post_write_hooks", {}),
  207. recursive_version_locations=rvl,
  208. messaging_opts=config.messaging_opts,
  209. )
  210. @contextmanager
  211. def _catch_revision_errors(
  212. self,
  213. ancestor: Optional[str] = None,
  214. multiple_heads: Optional[str] = None,
  215. start: Optional[str] = None,
  216. end: Optional[str] = None,
  217. resolution: Optional[str] = None,
  218. ) -> Iterator[None]:
  219. try:
  220. yield
  221. except revision.RangeNotAncestorError as rna:
  222. if start is None:
  223. start = cast(Any, rna.lower)
  224. if end is None:
  225. end = cast(Any, rna.upper)
  226. if not ancestor:
  227. ancestor = (
  228. "Requested range %(start)s:%(end)s does not refer to "
  229. "ancestor/descendant revisions along the same branch"
  230. )
  231. ancestor = ancestor % {"start": start, "end": end}
  232. raise util.CommandError(ancestor) from rna
  233. except revision.MultipleHeads as mh:
  234. if not multiple_heads:
  235. multiple_heads = (
  236. "Multiple head revisions are present for given "
  237. "argument '%(head_arg)s'; please "
  238. "specify a specific target revision, "
  239. "'<branchname>@%(head_arg)s' to "
  240. "narrow to a specific head, or 'heads' for all heads"
  241. )
  242. multiple_heads = multiple_heads % {
  243. "head_arg": end or mh.argument,
  244. "heads": util.format_as_comma(mh.heads),
  245. }
  246. raise util.CommandError(multiple_heads) from mh
  247. except revision.ResolutionError as re:
  248. if resolution is None:
  249. resolution = "Can't locate revision identified by '%s'" % (
  250. re.argument
  251. )
  252. raise util.CommandError(resolution) from re
  253. except revision.RevisionError as err:
  254. raise util.CommandError(err.args[0]) from err
  255. def walk_revisions(
  256. self, base: str = "base", head: str = "heads"
  257. ) -> Iterator[Script]:
  258. """Iterate through all revisions.
  259. :param base: the base revision, or "base" to start from the
  260. empty revision.
  261. :param head: the head revision; defaults to "heads" to indicate
  262. all head revisions. May also be "head" to indicate a single
  263. head revision.
  264. """
  265. with self._catch_revision_errors(start=base, end=head):
  266. for rev in self.revision_map.iterate_revisions(
  267. head, base, inclusive=True, assert_relative_length=False
  268. ):
  269. yield cast(Script, rev)
  270. def get_revisions(self, id_: _GetRevArg) -> Tuple[Script, ...]:
  271. """Return the :class:`.Script` instance with the given rev identifier,
  272. symbolic name, or sequence of identifiers.
  273. """
  274. with self._catch_revision_errors():
  275. return cast(
  276. Tuple[Script, ...],
  277. self.revision_map.get_revisions(id_),
  278. )
  279. def get_all_current(self, id_: Tuple[str, ...]) -> Set[Script]:
  280. with self._catch_revision_errors():
  281. return cast(Set[Script], self.revision_map._get_all_current(id_))
  282. def get_revision(self, id_: str) -> Script:
  283. """Return the :class:`.Script` instance with the given rev id.
  284. .. seealso::
  285. :meth:`.ScriptDirectory.get_revisions`
  286. """
  287. with self._catch_revision_errors():
  288. return cast(Script, self.revision_map.get_revision(id_))
  289. def as_revision_number(
  290. self, id_: Optional[str]
  291. ) -> Optional[Union[str, Tuple[str, ...]]]:
  292. """Convert a symbolic revision, i.e. 'head' or 'base', into
  293. an actual revision number."""
  294. with self._catch_revision_errors():
  295. rev, branch_name = self.revision_map._resolve_revision_number(id_)
  296. if not rev:
  297. # convert () to None
  298. return None
  299. elif id_ == "heads":
  300. return rev
  301. else:
  302. return rev[0]
  303. def iterate_revisions(
  304. self,
  305. upper: Union[str, Tuple[str, ...], None],
  306. lower: Union[str, Tuple[str, ...], None],
  307. **kw: Any,
  308. ) -> Iterator[Script]:
  309. """Iterate through script revisions, starting at the given
  310. upper revision identifier and ending at the lower.
  311. The traversal uses strictly the `down_revision`
  312. marker inside each migration script, so
  313. it is a requirement that upper >= lower,
  314. else you'll get nothing back.
  315. The iterator yields :class:`.Script` objects.
  316. .. seealso::
  317. :meth:`.RevisionMap.iterate_revisions`
  318. """
  319. return cast(
  320. Iterator[Script],
  321. self.revision_map.iterate_revisions(upper, lower, **kw),
  322. )
  323. def get_current_head(self) -> Optional[str]:
  324. """Return the current head revision.
  325. If the script directory has multiple heads
  326. due to branching, an error is raised;
  327. :meth:`.ScriptDirectory.get_heads` should be
  328. preferred.
  329. :return: a string revision number.
  330. .. seealso::
  331. :meth:`.ScriptDirectory.get_heads`
  332. """
  333. with self._catch_revision_errors(
  334. multiple_heads=(
  335. "The script directory has multiple heads (due to branching)."
  336. "Please use get_heads(), or merge the branches using "
  337. "alembic merge."
  338. )
  339. ):
  340. return self.revision_map.get_current_head()
  341. def get_heads(self) -> List[str]:
  342. """Return all "versioned head" revisions as strings.
  343. This is normally a list of length one,
  344. unless branches are present. The
  345. :meth:`.ScriptDirectory.get_current_head()` method
  346. can be used normally when a script directory
  347. has only one head.
  348. :return: a tuple of string revision numbers.
  349. """
  350. return list(self.revision_map.heads)
  351. def get_base(self) -> Optional[str]:
  352. """Return the "base" revision as a string.
  353. This is the revision number of the script that
  354. has a ``down_revision`` of None.
  355. If the script directory has multiple bases, an error is raised;
  356. :meth:`.ScriptDirectory.get_bases` should be
  357. preferred.
  358. """
  359. bases = self.get_bases()
  360. if len(bases) > 1:
  361. raise util.CommandError(
  362. "The script directory has multiple bases. "
  363. "Please use get_bases()."
  364. )
  365. elif bases:
  366. return bases[0]
  367. else:
  368. return None
  369. def get_bases(self) -> List[str]:
  370. """return all "base" revisions as strings.
  371. This is the revision number of all scripts that
  372. have a ``down_revision`` of None.
  373. """
  374. return list(self.revision_map.bases)
  375. def _upgrade_revs(
  376. self, destination: str, current_rev: str
  377. ) -> List[RevisionStep]:
  378. with self._catch_revision_errors(
  379. ancestor="Destination %(end)s is not a valid upgrade "
  380. "target from current head(s)",
  381. end=destination,
  382. ):
  383. revs = self.iterate_revisions(
  384. destination, current_rev, implicit_base=True
  385. )
  386. return [
  387. migration.MigrationStep.upgrade_from_script(
  388. self.revision_map, script
  389. )
  390. for script in reversed(list(revs))
  391. ]
  392. def _downgrade_revs(
  393. self, destination: str, current_rev: Optional[str]
  394. ) -> List[RevisionStep]:
  395. with self._catch_revision_errors(
  396. ancestor="Destination %(end)s is not a valid downgrade "
  397. "target from current head(s)",
  398. end=destination,
  399. ):
  400. revs = self.iterate_revisions(
  401. current_rev, destination, select_for_downgrade=True
  402. )
  403. return [
  404. migration.MigrationStep.downgrade_from_script(
  405. self.revision_map, script
  406. )
  407. for script in revs
  408. ]
  409. def _stamp_revs(
  410. self, revision: _RevIdType, heads: _RevIdType
  411. ) -> List[StampStep]:
  412. with self._catch_revision_errors(
  413. multiple_heads="Multiple heads are present; please specify a "
  414. "single target revision"
  415. ):
  416. heads_revs = self.get_revisions(heads)
  417. steps = []
  418. if not revision:
  419. revision = "base"
  420. filtered_heads: List[Script] = []
  421. for rev in util.to_tuple(revision):
  422. if rev:
  423. filtered_heads.extend(
  424. self.revision_map.filter_for_lineage(
  425. cast(Sequence[Script], heads_revs),
  426. rev,
  427. include_dependencies=True,
  428. )
  429. )
  430. filtered_heads = util.unique_list(filtered_heads)
  431. dests = self.get_revisions(revision) or [None]
  432. for dest in dests:
  433. if dest is None:
  434. # dest is 'base'. Return a "delete branch" migration
  435. # for all applicable heads.
  436. steps.extend(
  437. [
  438. migration.StampStep(
  439. head.revision,
  440. None,
  441. False,
  442. True,
  443. self.revision_map,
  444. )
  445. for head in filtered_heads
  446. ]
  447. )
  448. continue
  449. elif dest in filtered_heads:
  450. # the dest is already in the version table, do nothing.
  451. continue
  452. # figure out if the dest is a descendant or an
  453. # ancestor of the selected nodes
  454. descendants = set(
  455. self.revision_map._get_descendant_nodes([dest])
  456. )
  457. ancestors = set(self.revision_map._get_ancestor_nodes([dest]))
  458. if descendants.intersection(filtered_heads):
  459. # heads are above the target, so this is a downgrade.
  460. # we can treat them as a "merge", single step.
  461. assert not ancestors.intersection(filtered_heads)
  462. todo_heads = [head.revision for head in filtered_heads]
  463. step = migration.StampStep(
  464. todo_heads,
  465. dest.revision,
  466. False,
  467. False,
  468. self.revision_map,
  469. )
  470. steps.append(step)
  471. continue
  472. elif ancestors.intersection(filtered_heads):
  473. # heads are below the target, so this is an upgrade.
  474. # we can treat them as a "merge", single step.
  475. todo_heads = [head.revision for head in filtered_heads]
  476. step = migration.StampStep(
  477. todo_heads,
  478. dest.revision,
  479. True,
  480. False,
  481. self.revision_map,
  482. )
  483. steps.append(step)
  484. continue
  485. else:
  486. # destination is in a branch not represented,
  487. # treat it as new branch
  488. step = migration.StampStep(
  489. (), dest.revision, True, True, self.revision_map
  490. )
  491. steps.append(step)
  492. continue
  493. return steps
  494. def run_env(self) -> None:
  495. """Run the script environment.
  496. This basically runs the ``env.py`` script present
  497. in the migration environment. It is called exclusively
  498. by the command functions in :mod:`alembic.command`.
  499. """
  500. util.load_python_file(self.dir, "env.py")
  501. @property
  502. def env_py_location(self) -> str:
  503. return os.path.abspath(os.path.join(self.dir, "env.py"))
  504. def _generate_template(self, src: str, dest: str, **kw: Any) -> None:
  505. with util.status(
  506. f"Generating {os.path.abspath(dest)}", **self.messaging_opts
  507. ):
  508. util.template_to_file(src, dest, self.output_encoding, **kw)
  509. def _copy_file(self, src: str, dest: str) -> None:
  510. with util.status(
  511. f"Generating {os.path.abspath(dest)}", **self.messaging_opts
  512. ):
  513. shutil.copy(src, dest)
  514. def _ensure_directory(self, path: str) -> None:
  515. path = os.path.abspath(path)
  516. if not os.path.exists(path):
  517. with util.status(
  518. f"Creating directory {path}", **self.messaging_opts
  519. ):
  520. os.makedirs(path)
  521. def _generate_create_date(self) -> datetime.datetime:
  522. if self.timezone is not None:
  523. if ZoneInfo is None:
  524. raise util.CommandError(
  525. "Python >= 3.9 is required for timezone support or"
  526. "the 'backports.zoneinfo' package must be installed."
  527. )
  528. # First, assume correct capitalization
  529. try:
  530. tzinfo = ZoneInfo(self.timezone)
  531. except ZoneInfoNotFoundError:
  532. tzinfo = None
  533. if tzinfo is None:
  534. try:
  535. tzinfo = ZoneInfo(self.timezone.upper())
  536. except ZoneInfoNotFoundError:
  537. raise util.CommandError(
  538. "Can't locate timezone: %s" % self.timezone
  539. ) from None
  540. create_date = (
  541. datetime.datetime.utcnow()
  542. .replace(tzinfo=datetime.timezone.utc)
  543. .astimezone(tzinfo)
  544. )
  545. else:
  546. create_date = datetime.datetime.now()
  547. return create_date
  548. def generate_revision(
  549. self,
  550. revid: str,
  551. message: Optional[str],
  552. head: Optional[_RevIdType] = None,
  553. splice: Optional[bool] = False,
  554. branch_labels: Optional[_RevIdType] = None,
  555. version_path: Optional[str] = None,
  556. depends_on: Optional[_RevIdType] = None,
  557. **kw: Any,
  558. ) -> Optional[Script]:
  559. """Generate a new revision file.
  560. This runs the ``script.py.mako`` template, given
  561. template arguments, and creates a new file.
  562. :param revid: String revision id. Typically this
  563. comes from ``alembic.util.rev_id()``.
  564. :param message: the revision message, the one passed
  565. by the -m argument to the ``revision`` command.
  566. :param head: the head revision to generate against. Defaults
  567. to the current "head" if no branches are present, else raises
  568. an exception.
  569. :param splice: if True, allow the "head" version to not be an
  570. actual head; otherwise, the selected head must be a head
  571. (e.g. endpoint) revision.
  572. """
  573. if head is None:
  574. head = "head"
  575. try:
  576. Script.verify_rev_id(revid)
  577. except revision.RevisionError as err:
  578. raise util.CommandError(err.args[0]) from err
  579. with self._catch_revision_errors(
  580. multiple_heads=(
  581. "Multiple heads are present; please specify the head "
  582. "revision on which the new revision should be based, "
  583. "or perform a merge."
  584. )
  585. ):
  586. heads = cast(
  587. Tuple[Optional["Revision"], ...],
  588. self.revision_map.get_revisions(head),
  589. )
  590. for h in heads:
  591. assert h != "base" # type: ignore[comparison-overlap]
  592. if len(set(heads)) != len(heads):
  593. raise util.CommandError("Duplicate head revisions specified")
  594. create_date = self._generate_create_date()
  595. if version_path is None:
  596. if len(self._version_locations) > 1:
  597. for head_ in heads:
  598. if head_ is not None:
  599. assert isinstance(head_, Script)
  600. version_path = os.path.dirname(head_.path)
  601. break
  602. else:
  603. raise util.CommandError(
  604. "Multiple version locations present, "
  605. "please specify --version-path"
  606. )
  607. else:
  608. version_path = self.versions
  609. norm_path = os.path.normpath(os.path.abspath(version_path))
  610. for vers_path in self._version_locations:
  611. if os.path.normpath(vers_path) == norm_path:
  612. break
  613. else:
  614. raise util.CommandError(
  615. "Path %s is not represented in current "
  616. "version locations" % version_path
  617. )
  618. if self.version_locations:
  619. self._ensure_directory(version_path)
  620. path = self._rev_path(version_path, revid, message, create_date)
  621. if not splice:
  622. for head_ in heads:
  623. if head_ is not None and not head_.is_head:
  624. raise util.CommandError(
  625. "Revision %s is not a head revision; please specify "
  626. "--splice to create a new branch from this revision"
  627. % head_.revision
  628. )
  629. resolved_depends_on: Optional[List[str]]
  630. if depends_on:
  631. with self._catch_revision_errors():
  632. resolved_depends_on = [
  633. dep
  634. if dep in rev.branch_labels # maintain branch labels
  635. else rev.revision # resolve partial revision identifiers
  636. for rev, dep in [
  637. (not_none(self.revision_map.get_revision(dep)), dep)
  638. for dep in util.to_list(depends_on)
  639. ]
  640. ]
  641. else:
  642. resolved_depends_on = None
  643. self._generate_template(
  644. os.path.join(self.dir, "script.py.mako"),
  645. path,
  646. up_revision=str(revid),
  647. down_revision=revision.tuple_rev_as_scalar(
  648. tuple(h.revision if h is not None else None for h in heads)
  649. ),
  650. branch_labels=util.to_tuple(branch_labels),
  651. depends_on=revision.tuple_rev_as_scalar(resolved_depends_on),
  652. create_date=create_date,
  653. comma=util.format_as_comma,
  654. message=message if message is not None else ("empty message"),
  655. **kw,
  656. )
  657. post_write_hooks = self.hook_config
  658. if post_write_hooks:
  659. write_hooks._run_hooks(path, post_write_hooks)
  660. try:
  661. script = Script._from_path(self, path)
  662. except revision.RevisionError as err:
  663. raise util.CommandError(err.args[0]) from err
  664. if script is None:
  665. return None
  666. if branch_labels and not script.branch_labels:
  667. raise util.CommandError(
  668. "Version %s specified branch_labels %s, however the "
  669. "migration file %s does not have them; have you upgraded "
  670. "your script.py.mako to include the "
  671. "'branch_labels' section?"
  672. % (script.revision, branch_labels, script.path)
  673. )
  674. self.revision_map.add_revision(script)
  675. return script
  676. def _rev_path(
  677. self,
  678. path: str,
  679. rev_id: str,
  680. message: Optional[str],
  681. create_date: datetime.datetime,
  682. ) -> str:
  683. epoch = int(create_date.timestamp())
  684. slug = "_".join(_slug_re.findall(message or "")).lower()
  685. if len(slug) > self.truncate_slug_length:
  686. slug = slug[: self.truncate_slug_length].rsplit("_", 1)[0] + "_"
  687. filename = "%s.py" % (
  688. self.file_template
  689. % {
  690. "rev": rev_id,
  691. "slug": slug,
  692. "epoch": epoch,
  693. "year": create_date.year,
  694. "month": create_date.month,
  695. "day": create_date.day,
  696. "hour": create_date.hour,
  697. "minute": create_date.minute,
  698. "second": create_date.second,
  699. }
  700. )
  701. return os.path.join(path, filename)
  702. class Script(revision.Revision):
  703. """Represent a single revision file in a ``versions/`` directory.
  704. The :class:`.Script` instance is returned by methods
  705. such as :meth:`.ScriptDirectory.iterate_revisions`.
  706. """
  707. def __init__(self, module: ModuleType, rev_id: str, path: str):
  708. self.module = module
  709. self.path = path
  710. super().__init__(
  711. rev_id,
  712. module.down_revision,
  713. branch_labels=util.to_tuple(
  714. getattr(module, "branch_labels", None), default=()
  715. ),
  716. dependencies=util.to_tuple(
  717. getattr(module, "depends_on", None), default=()
  718. ),
  719. )
  720. module: ModuleType
  721. """The Python module representing the actual script itself."""
  722. path: str
  723. """Filesystem path of the script."""
  724. _db_current_indicator: Optional[bool] = None
  725. """Utility variable which when set will cause string output to indicate
  726. this is a "current" version in some database"""
  727. @property
  728. def doc(self) -> str:
  729. """Return the docstring given in the script."""
  730. return re.split("\n\n", self.longdoc)[0]
  731. @property
  732. def longdoc(self) -> str:
  733. """Return the docstring given in the script."""
  734. doc = self.module.__doc__
  735. if doc:
  736. if hasattr(self.module, "_alembic_source_encoding"):
  737. doc = doc.decode( # type: ignore[attr-defined]
  738. self.module._alembic_source_encoding
  739. )
  740. return doc.strip() # type: ignore[union-attr]
  741. else:
  742. return ""
  743. @property
  744. def log_entry(self) -> str:
  745. entry = "Rev: %s%s%s%s%s\n" % (
  746. self.revision,
  747. " (head)" if self.is_head else "",
  748. " (branchpoint)" if self.is_branch_point else "",
  749. " (mergepoint)" if self.is_merge_point else "",
  750. " (current)" if self._db_current_indicator else "",
  751. )
  752. if self.is_merge_point:
  753. entry += "Merges: %s\n" % (self._format_down_revision(),)
  754. else:
  755. entry += "Parent: %s\n" % (self._format_down_revision(),)
  756. if self.dependencies:
  757. entry += "Also depends on: %s\n" % (
  758. util.format_as_comma(self.dependencies)
  759. )
  760. if self.is_branch_point:
  761. entry += "Branches into: %s\n" % (
  762. util.format_as_comma(self.nextrev)
  763. )
  764. if self.branch_labels:
  765. entry += "Branch names: %s\n" % (
  766. util.format_as_comma(self.branch_labels),
  767. )
  768. entry += "Path: %s\n" % (self.path,)
  769. entry += "\n%s\n" % (
  770. "\n".join(" %s" % para for para in self.longdoc.splitlines())
  771. )
  772. return entry
  773. def __str__(self) -> str:
  774. return "%s -> %s%s%s%s, %s" % (
  775. self._format_down_revision(),
  776. self.revision,
  777. " (head)" if self.is_head else "",
  778. " (branchpoint)" if self.is_branch_point else "",
  779. " (mergepoint)" if self.is_merge_point else "",
  780. self.doc,
  781. )
  782. def _head_only(
  783. self,
  784. include_branches: bool = False,
  785. include_doc: bool = False,
  786. include_parents: bool = False,
  787. tree_indicators: bool = True,
  788. head_indicators: bool = True,
  789. ) -> str:
  790. text = self.revision
  791. if include_parents:
  792. if self.dependencies:
  793. text = "%s (%s) -> %s" % (
  794. self._format_down_revision(),
  795. util.format_as_comma(self.dependencies),
  796. text,
  797. )
  798. else:
  799. text = "%s -> %s" % (self._format_down_revision(), text)
  800. assert text is not None
  801. if include_branches and self.branch_labels:
  802. text += " (%s)" % util.format_as_comma(self.branch_labels)
  803. if head_indicators or tree_indicators:
  804. text += "%s%s%s" % (
  805. " (head)" if self._is_real_head else "",
  806. " (effective head)"
  807. if self.is_head and not self._is_real_head
  808. else "",
  809. " (current)" if self._db_current_indicator else "",
  810. )
  811. if tree_indicators:
  812. text += "%s%s" % (
  813. " (branchpoint)" if self.is_branch_point else "",
  814. " (mergepoint)" if self.is_merge_point else "",
  815. )
  816. if include_doc:
  817. text += ", %s" % self.doc
  818. return text
  819. def cmd_format(
  820. self,
  821. verbose: bool,
  822. include_branches: bool = False,
  823. include_doc: bool = False,
  824. include_parents: bool = False,
  825. tree_indicators: bool = True,
  826. ) -> str:
  827. if verbose:
  828. return self.log_entry
  829. else:
  830. return self._head_only(
  831. include_branches, include_doc, include_parents, tree_indicators
  832. )
  833. def _format_down_revision(self) -> str:
  834. if not self.down_revision:
  835. return "<base>"
  836. else:
  837. return util.format_as_comma(self._versioned_down_revisions)
  838. @classmethod
  839. def _from_path(
  840. cls, scriptdir: ScriptDirectory, path: str
  841. ) -> Optional[Script]:
  842. dir_, filename = os.path.split(path)
  843. return cls._from_filename(scriptdir, dir_, filename)
  844. @classmethod
  845. def _list_py_dir(cls, scriptdir: ScriptDirectory, path: str) -> List[str]:
  846. paths = []
  847. for root, dirs, files in os.walk(path, topdown=True):
  848. if root.endswith("__pycache__"):
  849. # a special case - we may include these files
  850. # if a `sourceless` option is specified
  851. continue
  852. for filename in sorted(files):
  853. paths.append(os.path.join(root, filename))
  854. if scriptdir.sourceless:
  855. # look for __pycache__
  856. py_cache_path = os.path.join(root, "__pycache__")
  857. if os.path.exists(py_cache_path):
  858. # add all files from __pycache__ whose filename is not
  859. # already in the names we got from the version directory.
  860. # add as relative paths including __pycache__ token
  861. names = {filename.split(".")[0] for filename in files}
  862. paths.extend(
  863. os.path.join(py_cache_path, pyc)
  864. for pyc in os.listdir(py_cache_path)
  865. if pyc.split(".")[0] not in names
  866. )
  867. if not scriptdir.recursive_version_locations:
  868. break
  869. # the real script order is defined by revision,
  870. # but it may be undefined if there are many files with a same
  871. # `down_revision`, for a better user experience (ex. debugging),
  872. # we use a deterministic order
  873. dirs.sort()
  874. return paths
  875. @classmethod
  876. def _from_filename(
  877. cls, scriptdir: ScriptDirectory, dir_: str, filename: str
  878. ) -> Optional[Script]:
  879. if scriptdir.sourceless:
  880. py_match = _sourceless_rev_file.match(filename)
  881. else:
  882. py_match = _only_source_rev_file.match(filename)
  883. if not py_match:
  884. return None
  885. py_filename = py_match.group(1)
  886. if scriptdir.sourceless:
  887. is_c = py_match.group(2) == "c"
  888. is_o = py_match.group(2) == "o"
  889. else:
  890. is_c = is_o = False
  891. if is_o or is_c:
  892. py_exists = os.path.exists(os.path.join(dir_, py_filename))
  893. pyc_exists = os.path.exists(os.path.join(dir_, py_filename + "c"))
  894. # prefer .py over .pyc because we'd like to get the
  895. # source encoding; prefer .pyc over .pyo because we'd like to
  896. # have the docstrings which a -OO file would not have
  897. if py_exists or is_o and pyc_exists:
  898. return None
  899. module = util.load_python_file(dir_, filename)
  900. if not hasattr(module, "revision"):
  901. # attempt to get the revision id from the script name,
  902. # this for legacy only
  903. m = _legacy_rev.match(filename)
  904. if not m:
  905. raise util.CommandError(
  906. "Could not determine revision id from filename %s. "
  907. "Be sure the 'revision' variable is "
  908. "declared inside the script (please see 'Upgrading "
  909. "from Alembic 0.1 to 0.2' in the documentation)."
  910. % filename
  911. )
  912. else:
  913. revision = m.group(1)
  914. else:
  915. revision = module.revision
  916. return Script(module, revision, os.path.join(dir_, filename))