base.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. import datetime
  2. from dateutil import tz
  3. import os
  4. import re
  5. import shutil
  6. from .. import util
  7. from ..util import compat
  8. from . import revision
  9. from ..runtime import migration
  10. from contextlib import contextmanager
  11. _sourceless_rev_file = re.compile(r'(?!\.\#|__init__)(.*\.py)(c|o)?$')
  12. _only_source_rev_file = re.compile(r'(?!\.\#|__init__)(.*\.py)$')
  13. _legacy_rev = re.compile(r'([a-f0-9]+)\.py$')
  14. _mod_def_re = re.compile(r'(upgrade|downgrade)_([a-z0-9]+)')
  15. _slug_re = re.compile(r'\w+')
  16. _default_file_template = "%(rev)s_%(slug)s"
  17. _split_on_space_comma = re.compile(r',|(?: +)')
  18. class ScriptDirectory(object):
  19. """Provides operations upon an Alembic script directory.
  20. This object is useful to get information as to current revisions,
  21. most notably being able to get at the "head" revision, for schemes
  22. that want to test if the current revision in the database is the most
  23. recent::
  24. from alembic.script import ScriptDirectory
  25. from alembic.config import Config
  26. config = Config()
  27. config.set_main_option("script_location", "myapp:migrations")
  28. script = ScriptDirectory.from_config(config)
  29. head_revision = script.get_current_head()
  30. """
  31. def __init__(self, dir, file_template=_default_file_template,
  32. truncate_slug_length=40,
  33. version_locations=None,
  34. sourceless=False, output_encoding="utf-8",
  35. timezone=None):
  36. self.dir = dir
  37. self.file_template = file_template
  38. self.version_locations = version_locations
  39. self.truncate_slug_length = truncate_slug_length or 40
  40. self.sourceless = sourceless
  41. self.output_encoding = output_encoding
  42. self.revision_map = revision.RevisionMap(self._load_revisions)
  43. self.timezone = timezone
  44. if not os.access(dir, os.F_OK):
  45. raise util.CommandError("Path doesn't exist: %r. Please use "
  46. "the 'init' command to create a new "
  47. "scripts folder." % dir)
  48. @property
  49. def versions(self):
  50. loc = self._version_locations
  51. if len(loc) > 1:
  52. raise util.CommandError("Multiple version_locations present")
  53. else:
  54. return loc[0]
  55. @util.memoized_property
  56. def _version_locations(self):
  57. if self.version_locations:
  58. return [
  59. os.path.abspath(util.coerce_resource_to_filename(location))
  60. for location in self.version_locations
  61. ]
  62. else:
  63. return (os.path.abspath(os.path.join(self.dir, 'versions')),)
  64. def _load_revisions(self):
  65. if self.version_locations:
  66. paths = [
  67. vers for vers in self._version_locations
  68. if os.path.exists(vers)]
  69. else:
  70. paths = [self.versions]
  71. dupes = set()
  72. for vers in paths:
  73. for file_ in os.listdir(vers):
  74. path = os.path.realpath(os.path.join(vers, file_))
  75. if path in dupes:
  76. util.warn(
  77. "File %s loaded twice! ignoring. Please ensure "
  78. "version_locations is unique." % path
  79. )
  80. continue
  81. dupes.add(path)
  82. script = Script._from_filename(self, vers, file_)
  83. if script is None:
  84. continue
  85. yield script
  86. @classmethod
  87. def from_config(cls, config):
  88. """Produce a new :class:`.ScriptDirectory` given a :class:`.Config`
  89. instance.
  90. The :class:`.Config` need only have the ``script_location`` key
  91. present.
  92. """
  93. script_location = config.get_main_option('script_location')
  94. if script_location is None:
  95. raise util.CommandError("No 'script_location' key "
  96. "found in configuration.")
  97. truncate_slug_length = config.get_main_option("truncate_slug_length")
  98. if truncate_slug_length is not None:
  99. truncate_slug_length = int(truncate_slug_length)
  100. version_locations = config.get_main_option("version_locations")
  101. if version_locations:
  102. version_locations = _split_on_space_comma.split(version_locations)
  103. return ScriptDirectory(
  104. util.coerce_resource_to_filename(script_location),
  105. file_template=config.get_main_option(
  106. 'file_template',
  107. _default_file_template),
  108. truncate_slug_length=truncate_slug_length,
  109. sourceless=config.get_main_option("sourceless") == "true",
  110. output_encoding=config.get_main_option("output_encoding", "utf-8"),
  111. version_locations=version_locations,
  112. timezone=config.get_main_option("timezone")
  113. )
  114. @contextmanager
  115. def _catch_revision_errors(
  116. self,
  117. ancestor=None, multiple_heads=None, start=None, end=None,
  118. resolution=None):
  119. try:
  120. yield
  121. except revision.RangeNotAncestorError as rna:
  122. if start is None:
  123. start = rna.lower
  124. if end is None:
  125. end = rna.upper
  126. if not ancestor:
  127. ancestor = (
  128. "Requested range %(start)s:%(end)s does not refer to "
  129. "ancestor/descendant revisions along the same branch"
  130. )
  131. ancestor = ancestor % {"start": start, "end": end}
  132. compat.raise_from_cause(util.CommandError(ancestor))
  133. except revision.MultipleHeads as mh:
  134. if not multiple_heads:
  135. multiple_heads = (
  136. "Multiple head revisions are present for given "
  137. "argument '%(head_arg)s'; please "
  138. "specify a specific target revision, "
  139. "'<branchname>@%(head_arg)s' to "
  140. "narrow to a specific head, or 'heads' for all heads")
  141. multiple_heads = multiple_heads % {
  142. "head_arg": end or mh.argument,
  143. "heads": util.format_as_comma(mh.heads)
  144. }
  145. compat.raise_from_cause(util.CommandError(multiple_heads))
  146. except revision.ResolutionError as re:
  147. if resolution is None:
  148. resolution = "Can't locate revision identified by '%s'" % (
  149. re.argument
  150. )
  151. compat.raise_from_cause(util.CommandError(resolution))
  152. except revision.RevisionError as err:
  153. compat.raise_from_cause(util.CommandError(err.args[0]))
  154. def walk_revisions(self, base="base", head="heads"):
  155. """Iterate through all revisions.
  156. :param base: the base revision, or "base" to start from the
  157. empty revision.
  158. :param head: the head revision; defaults to "heads" to indicate
  159. all head revisions. May also be "head" to indicate a single
  160. head revision.
  161. .. versionchanged:: 0.7.0 the "head" identifier now refers to
  162. the head of a non-branched repository only; use "heads" to
  163. refer to the set of all head branches simultaneously.
  164. """
  165. with self._catch_revision_errors(start=base, end=head):
  166. for rev in self.revision_map.iterate_revisions(
  167. head, base, inclusive=True, assert_relative_length=False):
  168. yield rev
  169. def get_revisions(self, id_):
  170. """Return the :class:`.Script` instance with the given rev identifier,
  171. symbolic name, or sequence of identifiers.
  172. .. versionadded:: 0.7.0
  173. """
  174. with self._catch_revision_errors():
  175. return self.revision_map.get_revisions(id_)
  176. def get_all_current(self, id_):
  177. with self._catch_revision_errors():
  178. top_revs = set(self.revision_map.get_revisions(id_))
  179. top_revs.update(
  180. self.revision_map._get_ancestor_nodes(
  181. list(top_revs), include_dependencies=True)
  182. )
  183. top_revs = self.revision_map._filter_into_branch_heads(top_revs)
  184. return top_revs
  185. def get_revision(self, id_):
  186. """Return the :class:`.Script` instance with the given rev id.
  187. .. seealso::
  188. :meth:`.ScriptDirectory.get_revisions`
  189. """
  190. with self._catch_revision_errors():
  191. return self.revision_map.get_revision(id_)
  192. def as_revision_number(self, id_):
  193. """Convert a symbolic revision, i.e. 'head' or 'base', into
  194. an actual revision number."""
  195. with self._catch_revision_errors():
  196. rev, branch_name = self.revision_map._resolve_revision_number(id_)
  197. if not rev:
  198. # convert () to None
  199. return None
  200. else:
  201. return rev[0]
  202. def iterate_revisions(self, upper, lower):
  203. """Iterate through script revisions, starting at the given
  204. upper revision identifier and ending at the lower.
  205. The traversal uses strictly the `down_revision`
  206. marker inside each migration script, so
  207. it is a requirement that upper >= lower,
  208. else you'll get nothing back.
  209. The iterator yields :class:`.Script` objects.
  210. .. seealso::
  211. :meth:`.RevisionMap.iterate_revisions`
  212. """
  213. return self.revision_map.iterate_revisions(upper, lower)
  214. def get_current_head(self):
  215. """Return the current head revision.
  216. If the script directory has multiple heads
  217. due to branching, an error is raised;
  218. :meth:`.ScriptDirectory.get_heads` should be
  219. preferred.
  220. :return: a string revision number.
  221. .. seealso::
  222. :meth:`.ScriptDirectory.get_heads`
  223. """
  224. with self._catch_revision_errors(multiple_heads=(
  225. 'The script directory has multiple heads (due to branching).'
  226. 'Please use get_heads(), or merge the branches using '
  227. 'alembic merge.'
  228. )):
  229. return self.revision_map.get_current_head()
  230. def get_heads(self):
  231. """Return all "versioned head" revisions as strings.
  232. This is normally a list of length one,
  233. unless branches are present. The
  234. :meth:`.ScriptDirectory.get_current_head()` method
  235. can be used normally when a script directory
  236. has only one head.
  237. :return: a tuple of string revision numbers.
  238. """
  239. return list(self.revision_map.heads)
  240. def get_base(self):
  241. """Return the "base" revision as a string.
  242. This is the revision number of the script that
  243. has a ``down_revision`` of None.
  244. If the script directory has multiple bases, an error is raised;
  245. :meth:`.ScriptDirectory.get_bases` should be
  246. preferred.
  247. """
  248. bases = self.get_bases()
  249. if len(bases) > 1:
  250. raise util.CommandError(
  251. "The script directory has multiple bases. "
  252. "Please use get_bases().")
  253. elif bases:
  254. return bases[0]
  255. else:
  256. return None
  257. def get_bases(self):
  258. """return all "base" revisions as strings.
  259. This is the revision number of all scripts that
  260. have a ``down_revision`` of None.
  261. .. versionadded:: 0.7.0
  262. """
  263. return list(self.revision_map.bases)
  264. def _upgrade_revs(self, destination, current_rev):
  265. with self._catch_revision_errors(
  266. ancestor="Destination %(end)s is not a valid upgrade "
  267. "target from current head(s)", end=destination):
  268. revs = self.revision_map.iterate_revisions(
  269. destination, current_rev, implicit_base=True)
  270. revs = list(revs)
  271. return [
  272. migration.MigrationStep.upgrade_from_script(
  273. self.revision_map, script)
  274. for script in reversed(list(revs))
  275. ]
  276. def _downgrade_revs(self, destination, current_rev):
  277. with self._catch_revision_errors(
  278. ancestor="Destination %(end)s is not a valid downgrade "
  279. "target from current head(s)", end=destination):
  280. revs = self.revision_map.iterate_revisions(
  281. current_rev, destination, select_for_downgrade=True)
  282. return [
  283. migration.MigrationStep.downgrade_from_script(
  284. self.revision_map, script)
  285. for script in revs
  286. ]
  287. def _stamp_revs(self, revision, heads):
  288. with self._catch_revision_errors(
  289. multiple_heads="Multiple heads are present; please specify a "
  290. "single target revision"):
  291. heads = self.get_revisions(heads)
  292. # filter for lineage will resolve things like
  293. # branchname@base, version@base, etc.
  294. filtered_heads = self.revision_map.filter_for_lineage(
  295. heads, revision, include_dependencies=True)
  296. steps = []
  297. dests = self.get_revisions(revision) or [None]
  298. for dest in dests:
  299. if dest is None:
  300. # dest is 'base'. Return a "delete branch" migration
  301. # for all applicable heads.
  302. steps.extend([
  303. migration.StampStep(head.revision, None, False, True)
  304. for head in filtered_heads
  305. ])
  306. continue
  307. elif dest in filtered_heads:
  308. # the dest is already in the version table, do nothing.
  309. continue
  310. # figure out if the dest is a descendant or an
  311. # ancestor of the selected nodes
  312. descendants = set(
  313. self.revision_map._get_descendant_nodes([dest]))
  314. ancestors = set(self.revision_map._get_ancestor_nodes([dest]))
  315. if descendants.intersection(filtered_heads):
  316. # heads are above the target, so this is a downgrade.
  317. # we can treat them as a "merge", single step.
  318. assert not ancestors.intersection(filtered_heads)
  319. todo_heads = [head.revision for head in filtered_heads]
  320. step = migration.StampStep(
  321. todo_heads, dest.revision, False, False)
  322. steps.append(step)
  323. continue
  324. elif ancestors.intersection(filtered_heads):
  325. # heads are below the target, so this is an upgrade.
  326. # we can treat them as a "merge", single step.
  327. todo_heads = [head.revision for head in filtered_heads]
  328. step = migration.StampStep(
  329. todo_heads, dest.revision, True, False)
  330. steps.append(step)
  331. continue
  332. else:
  333. # destination is in a branch not represented,
  334. # treat it as new branch
  335. step = migration.StampStep((), dest.revision, True, True)
  336. steps.append(step)
  337. continue
  338. return steps
  339. def run_env(self):
  340. """Run the script environment.
  341. This basically runs the ``env.py`` script present
  342. in the migration environment. It is called exclusively
  343. by the command functions in :mod:`alembic.command`.
  344. """
  345. util.load_python_file(self.dir, 'env.py')
  346. @property
  347. def env_py_location(self):
  348. return os.path.abspath(os.path.join(self.dir, "env.py"))
  349. def _generate_template(self, src, dest, **kw):
  350. util.status("Generating %s" % os.path.abspath(dest),
  351. util.template_to_file,
  352. src,
  353. dest,
  354. self.output_encoding,
  355. **kw
  356. )
  357. def _copy_file(self, src, dest):
  358. util.status("Generating %s" % os.path.abspath(dest),
  359. shutil.copy,
  360. src, dest)
  361. def _ensure_directory(self, path):
  362. path = os.path.abspath(path)
  363. if not os.path.exists(path):
  364. util.status(
  365. "Creating directory %s" % path,
  366. os.makedirs, path)
  367. def _generate_create_date(self):
  368. if self.timezone is not None:
  369. tzinfo = tz.gettz(self.timezone.upper())
  370. if tzinfo is None:
  371. raise util.CommandError(
  372. "Can't locate timezone: %s" % self.timezone)
  373. create_date = datetime.datetime.utcnow().replace(
  374. tzinfo=tz.tzutc()).astimezone(tzinfo)
  375. else:
  376. create_date = datetime.datetime.now()
  377. return create_date
  378. def generate_revision(
  379. self, revid, message, head=None,
  380. refresh=False, splice=False, branch_labels=None,
  381. version_path=None, depends_on=None, **kw):
  382. """Generate a new revision file.
  383. This runs the ``script.py.mako`` template, given
  384. template arguments, and creates a new file.
  385. :param revid: String revision id. Typically this
  386. comes from ``alembic.util.rev_id()``.
  387. :param message: the revision message, the one passed
  388. by the -m argument to the ``revision`` command.
  389. :param head: the head revision to generate against. Defaults
  390. to the current "head" if no branches are present, else raises
  391. an exception.
  392. .. versionadded:: 0.7.0
  393. :param splice: if True, allow the "head" version to not be an
  394. actual head; otherwise, the selected head must be a head
  395. (e.g. endpoint) revision.
  396. :param refresh: deprecated.
  397. """
  398. if head is None:
  399. head = "head"
  400. with self._catch_revision_errors(multiple_heads=(
  401. "Multiple heads are present; please specify the head "
  402. "revision on which the new revision should be based, "
  403. "or perform a merge."
  404. )):
  405. heads = self.revision_map.get_revisions(head)
  406. if len(set(heads)) != len(heads):
  407. raise util.CommandError("Duplicate head revisions specified")
  408. create_date = self._generate_create_date()
  409. if version_path is None:
  410. if len(self._version_locations) > 1:
  411. for head in heads:
  412. if head is not None:
  413. version_path = os.path.dirname(head.path)
  414. break
  415. else:
  416. raise util.CommandError(
  417. "Multiple version locations present, "
  418. "please specify --version-path")
  419. else:
  420. version_path = self.versions
  421. norm_path = os.path.normpath(os.path.abspath(version_path))
  422. for vers_path in self._version_locations:
  423. if os.path.normpath(vers_path) == norm_path:
  424. break
  425. else:
  426. raise util.CommandError(
  427. "Path %s is not represented in current "
  428. "version locations" % version_path)
  429. if self.version_locations:
  430. self._ensure_directory(version_path)
  431. path = self._rev_path(version_path, revid, message, create_date)
  432. if not splice:
  433. for head in heads:
  434. if head is not None and not head.is_head:
  435. raise util.CommandError(
  436. "Revision %s is not a head revision; please specify "
  437. "--splice to create a new branch from this revision"
  438. % head.revision)
  439. if depends_on:
  440. with self._catch_revision_errors():
  441. depends_on = [
  442. dep
  443. if dep in rev.branch_labels # maintain branch labels
  444. else rev.revision # resolve partial revision identifiers
  445. for rev, dep in [
  446. (self.revision_map.get_revision(dep), dep)
  447. for dep in util.to_list(depends_on)
  448. ]
  449. ]
  450. self._generate_template(
  451. os.path.join(self.dir, "script.py.mako"),
  452. path,
  453. up_revision=str(revid),
  454. down_revision=revision.tuple_rev_as_scalar(
  455. tuple(h.revision if h is not None else None for h in heads)),
  456. branch_labels=util.to_tuple(branch_labels),
  457. depends_on=revision.tuple_rev_as_scalar(depends_on),
  458. create_date=create_date,
  459. comma=util.format_as_comma,
  460. message=message if message is not None else ("empty message"),
  461. **kw
  462. )
  463. script = Script._from_path(self, path)
  464. if branch_labels and not script.branch_labels:
  465. raise util.CommandError(
  466. "Version %s specified branch_labels %s, however the "
  467. "migration file %s does not have them; have you upgraded "
  468. "your script.py.mako to include the "
  469. "'branch_labels' section?" % (
  470. script.revision, branch_labels, script.path
  471. ))
  472. self.revision_map.add_revision(script)
  473. return script
  474. def _rev_path(self, path, rev_id, message, create_date):
  475. slug = "_".join(_slug_re.findall(message or "")).lower()
  476. if len(slug) > self.truncate_slug_length:
  477. slug = slug[:self.truncate_slug_length].rsplit('_', 1)[0] + '_'
  478. filename = "%s.py" % (
  479. self.file_template % {
  480. 'rev': rev_id,
  481. 'slug': slug,
  482. 'year': create_date.year,
  483. 'month': create_date.month,
  484. 'day': create_date.day,
  485. 'hour': create_date.hour,
  486. 'minute': create_date.minute,
  487. 'second': create_date.second
  488. }
  489. )
  490. return os.path.join(path, filename)
  491. class Script(revision.Revision):
  492. """Represent a single revision file in a ``versions/`` directory.
  493. The :class:`.Script` instance is returned by methods
  494. such as :meth:`.ScriptDirectory.iterate_revisions`.
  495. """
  496. def __init__(self, module, rev_id, path):
  497. self.module = module
  498. self.path = path
  499. super(Script, self).__init__(
  500. rev_id,
  501. module.down_revision,
  502. branch_labels=util.to_tuple(
  503. getattr(module, 'branch_labels', None), default=()),
  504. dependencies=util.to_tuple(
  505. getattr(module, 'depends_on', None), default=())
  506. )
  507. module = None
  508. """The Python module representing the actual script itself."""
  509. path = None
  510. """Filesystem path of the script."""
  511. @property
  512. def doc(self):
  513. """Return the docstring given in the script."""
  514. return re.split("\n\n", self.longdoc)[0]
  515. @property
  516. def longdoc(self):
  517. """Return the docstring given in the script."""
  518. doc = self.module.__doc__
  519. if doc:
  520. if hasattr(self.module, "_alembic_source_encoding"):
  521. doc = doc.decode(self.module._alembic_source_encoding)
  522. return doc.strip()
  523. else:
  524. return ""
  525. @property
  526. def log_entry(self):
  527. entry = "Rev: %s%s%s%s\n" % (
  528. self.revision,
  529. " (head)" if self.is_head else "",
  530. " (branchpoint)" if self.is_branch_point else "",
  531. " (mergepoint)" if self.is_merge_point else "",
  532. )
  533. if self.is_merge_point:
  534. entry += "Merges: %s\n" % (self._format_down_revision(), )
  535. else:
  536. entry += "Parent: %s\n" % (self._format_down_revision(), )
  537. if self.dependencies:
  538. entry += "Also depends on: %s\n" % (
  539. util.format_as_comma(self.dependencies))
  540. if self.is_branch_point:
  541. entry += "Branches into: %s\n" % (
  542. util.format_as_comma(self.nextrev))
  543. if self.branch_labels:
  544. entry += "Branch names: %s\n" % (
  545. util.format_as_comma(self.branch_labels), )
  546. entry += "Path: %s\n" % (self.path,)
  547. entry += "\n%s\n" % (
  548. "\n".join(
  549. " %s" % para
  550. for para in self.longdoc.splitlines()
  551. )
  552. )
  553. return entry
  554. def __str__(self):
  555. return "%s -> %s%s%s%s, %s" % (
  556. self._format_down_revision(),
  557. self.revision,
  558. " (head)" if self.is_head else "",
  559. " (branchpoint)" if self.is_branch_point else "",
  560. " (mergepoint)" if self.is_merge_point else "",
  561. self.doc)
  562. def _head_only(
  563. self, include_branches=False, include_doc=False,
  564. include_parents=False, tree_indicators=True,
  565. head_indicators=True):
  566. text = self.revision
  567. if include_parents:
  568. if self.dependencies:
  569. text = "%s (%s) -> %s" % (
  570. self._format_down_revision(),
  571. util.format_as_comma(self.dependencies),
  572. text
  573. )
  574. else:
  575. text = "%s -> %s" % (
  576. self._format_down_revision(), text)
  577. if include_branches and self.branch_labels:
  578. text += " (%s)" % util.format_as_comma(self.branch_labels)
  579. if head_indicators or tree_indicators:
  580. text += "%s%s" % (
  581. " (head)" if self._is_real_head else "",
  582. " (effective head)" if self.is_head and
  583. not self._is_real_head else ""
  584. )
  585. if tree_indicators:
  586. text += "%s%s" % (
  587. " (branchpoint)" if self.is_branch_point else "",
  588. " (mergepoint)" if self.is_merge_point else ""
  589. )
  590. if include_doc:
  591. text += ", %s" % self.doc
  592. return text
  593. def cmd_format(
  594. self,
  595. verbose,
  596. include_branches=False, include_doc=False,
  597. include_parents=False, tree_indicators=True):
  598. if verbose:
  599. return self.log_entry
  600. else:
  601. return self._head_only(
  602. include_branches, include_doc,
  603. include_parents, tree_indicators)
  604. def _format_down_revision(self):
  605. if not self.down_revision:
  606. return "<base>"
  607. else:
  608. return util.format_as_comma(self._versioned_down_revisions)
  609. @classmethod
  610. def _from_path(cls, scriptdir, path):
  611. dir_, filename = os.path.split(path)
  612. return cls._from_filename(scriptdir, dir_, filename)
  613. @classmethod
  614. def _from_filename(cls, scriptdir, dir_, filename):
  615. if scriptdir.sourceless:
  616. py_match = _sourceless_rev_file.match(filename)
  617. else:
  618. py_match = _only_source_rev_file.match(filename)
  619. if not py_match:
  620. return None
  621. py_filename = py_match.group(1)
  622. if scriptdir.sourceless:
  623. is_c = py_match.group(2) == 'c'
  624. is_o = py_match.group(2) == 'o'
  625. else:
  626. is_c = is_o = False
  627. if is_o or is_c:
  628. py_exists = os.path.exists(os.path.join(dir_, py_filename))
  629. pyc_exists = os.path.exists(os.path.join(dir_, py_filename + "c"))
  630. # prefer .py over .pyc because we'd like to get the
  631. # source encoding; prefer .pyc over .pyo because we'd like to
  632. # have the docstrings which a -OO file would not have
  633. if py_exists or is_o and pyc_exists:
  634. return None
  635. module = util.load_python_file(dir_, filename)
  636. if not hasattr(module, "revision"):
  637. # attempt to get the revision id from the script name,
  638. # this for legacy only
  639. m = _legacy_rev.match(filename)
  640. if not m:
  641. raise util.CommandError(
  642. "Could not determine revision id from filename %s. "
  643. "Be sure the 'revision' variable is "
  644. "declared inside the script (please see 'Upgrading "
  645. "from Alembic 0.1 to 0.2' in the documentation)."
  646. % filename)
  647. else:
  648. revision = m.group(1)
  649. else:
  650. revision = module.revision
  651. return Script(module, revision, os.path.join(dir_, filename))