bump_version.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. #!/usr/bin/env python
  2. from __future__ import absolute_import
  3. import errno
  4. import os
  5. import re
  6. import shlex
  7. import subprocess
  8. import sys
  9. from contextlib import contextmanager
  10. from tempfile import NamedTemporaryFile
  11. rq = lambda s: s.strip("\"'")
  12. def cmd(*args):
  13. return subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]
  14. @contextmanager
  15. def no_enoent():
  16. try:
  17. yield
  18. except OSError, exc:
  19. if exc.errno != errno.ENOENT:
  20. raise
  21. class StringVersion(object):
  22. def decode(self, s):
  23. s = rq(s)
  24. text = ""
  25. major, minor, release = s.split(".")
  26. if not release.isdigit():
  27. pos = release.index(re.split("\d+", release)[1][0])
  28. release, text = release[:pos], release[pos:]
  29. return int(major), int(minor), int(release), text
  30. def encode(self, v):
  31. return ".".join(map(str, v[:3])) + v[3]
  32. to_str = StringVersion().encode
  33. from_str = StringVersion().decode
  34. class TupleVersion(object):
  35. def decode(self, s):
  36. v = list(map(rq, s.split(", ")))
  37. return (tuple(map(int, v[0:3])) +
  38. tuple(["".join(v[3:])]))
  39. def encode(self, v):
  40. v = list(v)
  41. def quote(lit):
  42. if isinstance(lit, basestring):
  43. return '"{0}"'.format(lit)
  44. return str(lit)
  45. if not v[-1]:
  46. v.pop()
  47. return ", ".join(map(quote, v))
  48. class VersionFile(object):
  49. def __init__(self, filename):
  50. self.filename = filename
  51. self._kept = None
  52. def _as_orig(self, version):
  53. return self.wb.format(version=self.type.encode(version),
  54. kept=self._kept)
  55. def write(self, version):
  56. pattern = self.regex
  57. with no_enoent():
  58. with NamedTemporaryFile() as dest:
  59. with open(self.filename) as orig:
  60. for line in orig:
  61. if pattern.match(line):
  62. dest.write(self._as_orig(version))
  63. else:
  64. dest.write(line)
  65. os.rename(dest.name, self.filename)
  66. def parse(self):
  67. pattern = self.regex
  68. gpos = 0
  69. with open(self.filename) as fh:
  70. for line in fh:
  71. m = pattern.match(line)
  72. if m:
  73. if "?P<keep>" in pattern.pattern:
  74. self._kept, gpos = m.groupdict()["keep"], 1
  75. return self.type.decode(m.groups()[gpos])
  76. class PyVersion(VersionFile):
  77. regex = re.compile(r'^VERSION\s*=\s*\((.+?)\)')
  78. wb = "VERSION = ({version})\n"
  79. type = TupleVersion()
  80. class SphinxVersion(VersionFile):
  81. regex = re.compile(r'^:[Vv]ersion:\s*(.+?)$')
  82. wb = ':Version: {version}\n'
  83. type = StringVersion()
  84. class CPPVersion(VersionFile):
  85. regex = re.compile(r'^\#\s*define\s*(?P<keep>\w*)VERSION\s+(.+)')
  86. wb = '#define {kept}VERSION "{version}"\n'
  87. type = StringVersion()
  88. _filetype_to_type = {"py": PyVersion,
  89. "rst": SphinxVersion,
  90. "txt": SphinxVersion,
  91. "c": CPPVersion,
  92. "h": CPPVersion}
  93. def filetype_to_type(filename):
  94. _, _, suffix = filename.rpartition(".")
  95. return _filetype_to_type[suffix](filename)
  96. def bump(*files, **kwargs):
  97. version = kwargs.get("version")
  98. before_commit = kwargs.get("before_commit")
  99. files = [filetype_to_type(f) for f in files]
  100. versions = [v.parse() for v in files]
  101. current = list(reversed(sorted(versions)))[0] # find highest
  102. current = current.split()[0] # only first sentence
  103. if version:
  104. next = from_str(version)
  105. else:
  106. major, minor, release, text = current
  107. if text:
  108. raise Exception("Can't bump alpha releases")
  109. next = (major, minor, release + 1, text)
  110. print("Bump version from {0} -> {1}".format(to_str(current), to_str(next)))
  111. for v in files:
  112. print(" writing {0.filename!r}...".format(v))
  113. v.write(next)
  114. if before_commit:
  115. cmd(*shlex.split(before_commit))
  116. print(cmd("git", "commit", "-m", "Bumps version to {0}".format(
  117. to_str(next)), *[f.filename for f in files]))
  118. print(cmd("git", "tag", "v{0}".format(to_str(next))))
  119. def main(argv=sys.argv, version=None, before_commit=None):
  120. if not len(argv) > 1:
  121. print("Usage: distdir [docfile] -- <custom version>")
  122. sys.exit(0)
  123. args = []
  124. for arg in argv:
  125. if arg.startswith("--before-commit="):
  126. _, before_commit = arg.split('=')
  127. else:
  128. args.append(arg)
  129. if "--" in args:
  130. c = args.index('--')
  131. version = args[c + 1]
  132. argv = args[:c]
  133. bump(*args[1:], version=version, before_commit=before_commit)
  134. if __name__ == "__main__":
  135. main()