Name: js-handler/node_modules/restify/node_modules/bunyan/tools/cutarelease.py
| 1: | #!/usr/bin/env python |
| 2: | # -*- coding: utf-8 -*- |
| 3: | # Copyright (c) 2009-2012 Trent Mick |
| 4: | |
| 5: | """cutarelease -- Cut a release of your project. |
| 6: | |
| 7: | A script that will help cut a release for a git-based project that follows |
| 8: | a few conventions. It'll update your changelog (CHANGES.md), add a git |
| 9: | tag, push those changes, update your version to the next patch level release |
| 10: | and create a new changelog section for that new version. |
| 11: | |
| 12: | Conventions: |
| 13: | - XXX |
| 14: | """ |
| 15: | |
| 16: | __version_info__ = (1, 0, 7) |
| 17: | __version__ = '.'.join(map(str, __version_info__)) |
| 18: | |
| 19: | import sys |
| 20: | import os |
| 21: | from os.path import join, dirname, normpath, abspath, exists, basename, splitext |
| 22: | from glob import glob |
| 23: | from pprint import pprint |
| 24: | import re |
| 25: | import codecs |
| 26: | import logging |
| 27: | import optparse |
| 28: | import json |
| 29: | |
| 30: | |
| 31: | |
| 32: | #---- globals and config |
| 33: | |
| 34: | log = logging.getLogger("cutarelease") |
| 35: | |
| 36: | class Error(Exception): |
| 37: | pass |
| 38: | |
| 39: | |
| 40: | |
| 41: | #---- main functionality |
| 42: | |
| 43: | def cutarelease(project_name, version_files, dry_run=False): |
| 44: | """Cut a release. |
| 45: | |
| 46: | @param project_name {str} |
| 47: | @param version_files {list} List of paths to files holding the version |
| 48: | info for this project. |
| 49: | |
| 50: | If none are given it attempts to guess the version file: |
| 51: | package.json or VERSION.txt or VERSION or $project_name.py |
| 52: | or lib/$project_name.py or $project_name.js or lib/$project_name.js. |
| 53: | |
| 54: | The version file can be in one of the following forms: |
| 55: | |
| 56: | - A .py file, in which case the file is expect to have a top-level |
| 57: | global called "__version_info__" as follows. [1] |
| 58: | |
| 59: | __version_info__ = (0, 7, 6) |
| 60: | |
| 61: | Note that I typically follow that with the following to get a |
| 62: | string version attribute on my modules: |
| 63: | |
| 64: | __version__ = '.'.join(map(str, __version_info__)) |
| 65: | |
| 66: | - A .js file, in which case the file is expected to have a top-level |
| 67: | global called "VERSION" as follows: |
| 68: | |
| 69: | ver VERSION = "1.2.3"; |
| 70: | |
| 71: | - A "package.json" file, typical of a node.js npm-using project. |
| 72: | The package.json file must have a "version" field. |
| 73: | |
| 74: | - TODO: A simple version file whose only content is a "1.2.3"-style version |
| 75: | string. |
| 76: | |
| 77: | [1]: This is a convention I tend to follow in my projects. |
| 78: | Granted it might not be your cup of tea. I should add support for |
| 79: | just `__version__ = "1.2.3"`. I'm open to other suggestions too. |
| 80: | """ |
| 81: | dry_run_str = dry_run and " (dry-run)" or "" |
| 82: | |
| 83: | if not version_files: |
| 84: | log.info("guessing version file") |
| 85: | candidates = [ |
| 86: | "package.json", |
| 87: | "VERSION.txt", |
| 88: | "VERSION", |
| 89: | "%s.py" % project_name, |
| 90: | "lib/%s.py" % project_name, |
| 91: | "%s.js" % project_name, |
| 92: | "lib/%s.js" % project_name, |
| 93: | ] |
| 94: | for candidate in candidates: |
| 95: | if exists(candidate): |
| 96: | version_files = [candidate] |
| 97: | break |
| 98: | else: |
| 99: | raise Error("could not find a version file: specify its path or " |
| 100: | "add one of the following to your project: '%s'" |
| 101: | % "', '".join(candidates)) |
| 102: | log.info("using '%s' as version file", version_files[0]) |
| 103: | |
| 104: | parsed_version_files = [_parse_version_file(f) for f in version_files] |
| 105: | version_file_type, version_info = parsed_version_files[0] |
| 106: | version = _version_from_version_info(version_info) |
| 107: | |
| 108: | # Confirm |
| 109: | if not dry_run: |
| 110: | answer = query_yes_no("* * *\n" |
| 111: | "Are you sure you want cut a %s release?\n" |
| 112: | "This will involved commits and a push." % version, |
| 113: | default="no") |
| 114: | print "* * *" |
| 115: | if answer != "yes": |
| 116: | log.info("user abort") |
| 117: | return |
| 118: | log.info("cutting a %s release%s", version, dry_run_str) |
| 119: | |
| 120: | # Checks: Ensure there is a section in changes for this version. |
| 121: | |
| 122: | |
| 123: | |
| 124: | changes_path = "CHANGES.md" |
| 125: | changes_txt, changes, nyr = parse_changelog(changes_path) |
| 126: | #pprint(changes) |
| 127: | top_ver = changes[0]["version"] |
| 128: | if top_ver != version: |
| 129: | raise Error("changelog '%s' top section says " |
| 130: | "version %r, expected version %r: aborting" |
| 131: | % (changes_path, top_ver, version)) |
| 132: | top_verline = changes[0]["verline"] |
| 133: | if not top_verline.endswith(nyr): |
| 134: | answer = query_yes_no("\n* * *\n" |
| 135: | "The changelog '%s' top section doesn't have the expected\n" |
| 136: | "'%s' marker. Has this been released already?" |
| 137: | % (changes_path, nyr), default="yes") |
| 138: | print "* * *" |
| 139: | if answer != "no": |
| 140: | log.info("abort") |
| 141: | return |
| 142: | top_body = changes[0]["body"] |
| 143: | if top_body.strip() == "(nothing yet)": |
| 144: | raise Error("top section body is `(nothing yet)': it looks like " |
| 145: | "nothing has been added to this release") |
| 146: | |
| 147: | # Commits to prepare release. |
| 148: | changes_txt_before = changes_txt |
| 149: | changes_txt = changes_txt.replace(" (not yet released)", "", 1) |
| 150: | if not dry_run and changes_txt != changes_txt_before: |
| 151: | log.info("prepare `%s' for release", changes_path) |
| 152: | f = codecs.open(changes_path, 'w', 'utf-8') |
| 153: | f.write(changes_txt) |
| 154: | f.close() |
| 155: | run('git commit %s -m "prepare for %s release"' |
| 156: | % (changes_path, version)) |
| 157: | |
| 158: | # Tag version and push. |
| 159: | curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t) |
| 160: | if not dry_run and version not in curr_tags: |
| 161: | log.info("tag the release") |
| 162: | run('git tag -a "%s" -m "version %s"' % (version, version)) |
| 163: | run('git push --tags') |
| 164: | |
| 165: | # Optionally release. |
| 166: | if exists("package.json"): |
| 167: | answer = query_yes_no("\n* * *\nPublish to npm?", default="yes") |
| 168: | print "* * *" |
| 169: | if answer == "yes": |
| 170: | if dry_run: |
| 171: | log.info("skipping npm publish (dry-run)") |
| 172: | else: |
| 173: | run('npm publish') |
| 174: | elif exists("setup.py"): |
| 175: | answer = query_yes_no("\n* * *\nPublish to pypi?", default="yes") |
| 176: | print "* * *" |
| 177: | if answer == "yes": |
| 178: | if dry_run: |
| 179: | log.info("skipping pypi publish (dry-run)") |
| 180: | else: |
| 181: | run("%spython setup.py sdist --formats zip upload" |
| 182: | % _setup_command_prefix()) |
| 183: | |
| 184: | # Commits to prepare for future dev and push. |
| 185: | # - update changelog file |
| 186: | next_version_info = _get_next_version_info(version_info) |
| 187: | next_version = _version_from_version_info(next_version_info) |
| 188: | log.info("prepare for future dev (version %s)", next_version) |
| 189: | marker = "## " + changes[0]["verline"] |
| 190: | if marker.endswith(nyr): |
| 191: | marker = marker[0:-len(nyr)] |
| 192: | if marker not in changes_txt: |
| 193: | raise Error("couldn't find `%s' marker in `%s' " |
| 194: | "content: can't prep for subsequent dev" % (marker, changes_path)) |
| 195: | next_verline = "%s %s%s" % (marker.rsplit(None, 1)[0], next_version, nyr) |
| 196: | changes_txt = changes_txt.replace(marker + '\n', |
| 197: | "%s\n\n(nothing yet)\n\n\n%s\n" % (next_verline, marker)) |
| 198: | if not dry_run: |
| 199: | f = codecs.open(changes_path, 'w', 'utf-8') |
| 200: | f.write(changes_txt) |
| 201: | f.close() |
| 202: | |
| 203: | # - update version file |
| 204: | next_version_tuple = _tuple_from_version(next_version) |
| 205: | for i, ver_file in enumerate(version_files): |
| 206: | ver_content = codecs.open(ver_file, 'r', 'utf-8').read() |
| 207: | ver_file_type, ver_info = parsed_version_files[i] |
| 208: | if ver_file_type == "json": |
| 209: | marker = '"version": "%s"' % version |
| 210: | if marker not in ver_content: |
| 211: | raise Error("couldn't find `%s' version marker in `%s' " |
| 212: | "content: can't prep for subsequent dev" % (marker, ver_file)) |
| 213: | ver_content = ver_content.replace(marker, |
| 214: | '"version": "%s"' % next_version) |
| 215: | elif ver_file_type == "javascript": |
| 216: | candidates = [ |
| 217: | ("single", "var VERSION = '%s';" % version), |
| 218: | ("double", 'var VERSION = "%s";' % version), |
| 219: | ] |
| 220: | for quote_type, marker in candidates: |
| 221: | if marker in ver_content: |
| 222: | break |
| 223: | else: |
| 224: | raise Error("couldn't find any candidate version marker in " |
| 225: | "`%s' content: can't prep for subsequent dev: %r" |
| 226: | % (ver_file, candidates)) |
| 227: | if quote_type == "single": |
| 228: | ver_content = ver_content.replace(marker, |
| 229: | "var VERSION = '%s';" % next_version) |
| 230: | else: |
| 231: | ver_content = ver_content.replace(marker, |
| 232: | 'var VERSION = "%s";' % next_version) |
| 233: | elif ver_file_type == "python": |
| 234: | marker = "__version_info__ = %r" % (version_info,) |
| 235: | if marker not in ver_content: |
| 236: | raise Error("couldn't find `%s' version marker in `%s' " |
| 237: | "content: can't prep for subsequent dev" % (marker, ver_file)) |
| 238: | ver_content = ver_content.replace(marker, |
| 239: | "__version_info__ = %r" % (next_version_tuple,)) |
| 240: | elif ver_file_type == "version": |
| 241: | ver_content = next_version |
| 242: | else: |
| 243: | raise Error("unknown ver_file_type: %r" % ver_file_type) |
| 244: | if not dry_run: |
| 245: | log.info("update version to '%s' in '%s'", next_version, ver_file) |
| 246: | f = codecs.open(ver_file, 'w', 'utf-8') |
| 247: | f.write(ver_content) |
| 248: | f.close() |
| 249: | |
| 250: | if not dry_run: |
| 251: | run('git commit %s %s -m "prep for future dev"' % ( |
| 252: | changes_path, ' '.join(version_files))) |
| 253: | run('git push') |
| 254: | |
| 255: | |
| 256: | |
| 257: | #---- internal support routines |
| 258: | |
| 259: | def _indent(s, indent=' '): |
| 260: | return indent + indent.join(s.splitlines(True)) |
| 261: | |
| 262: | def _tuple_from_version(version): |
| 263: | def _intify(s): |
| 264: | try: |
| 265: | return int(s) |
| 266: | except ValueError: |
| 267: | return s |
| 268: | return tuple(_intify(b) for b in version.split('.')) |
| 269: | |
| 270: | def _get_next_version_info(version_info): |
| 271: | next = list(version_info[:]) |
| 272: | next[-1] += 1 |
| 273: | return tuple(next) |
| 274: | |
| 275: | def _version_from_version_info(version_info): |
| 276: | v = str(version_info[0]) |
| 277: | state_dot_join = True |
| 278: | for i in version_info[1:]: |
| 279: | if state_dot_join: |
| 280: | try: |
| 281: | int(i) |
| 282: | except ValueError: |
| 283: | state_dot_join = False |
| 284: | else: |
| 285: | pass |
| 286: | if state_dot_join: |
| 287: | v += "." + str(i) |
| 288: | else: |
| 289: | v += str(i) |
| 290: | return v |
| 291: | |
| 292: | _version_re = re.compile(r"^(\d+)\.(\d+)(?:\.(\d+)([abc](\d+)?)?)?$") |
| 293: | def _version_info_from_version(version): |
| 294: | m = _version_re.match(version) |
| 295: | if not m: |
| 296: | raise Error("could not convert '%s' version to version info" % version) |
| 297: | version_info = [] |
| 298: | for g in m.groups(): |
| 299: | if g is None: |
| 300: | break |
| 301: | try: |
| 302: | version_info.append(int(g)) |
| 303: | except ValueError: |
| 304: | version_info.append(g) |
| 305: | return tuple(version_info) |
| 306: | |
| 307: | def _parse_version_file(version_file): |
| 308: | """Get version info from the given file. It can be any of: |
| 309: | |
| 310: | Supported version file types (i.e. types of files from which we know |
| 311: | how to parse the version string/number -- often by some convention): |
| 312: | - json: use the "version" key |
| 313: | - javascript: look for a `var VERSION = "1.2.3";` or |
| 314: | `var VERSION = '1.2.3';` |
| 315: | - python: Python script/module with `__version_info__ = (1, 2, 3)` |
| 316: | - version: a VERSION.txt or VERSION file where the whole contents are |
| 317: | the version string |
| 318: | |
| 319: | @param version_file {str} Can be a path or "type:path", where "type" |
| 320: | is one of the supported types. |
| 321: | """ |
| 322: | # Get version file *type*. |
| 323: | version_file_type = None |
| 324: | match = re.compile("^([a-z]+):(.*)$").search(version_file) |
| 325: | if match: |
| 326: | version_file = match.group(2) |
| 327: | version_file_type = match.group(1) |
| 328: | aliases = { |
| 329: | "js": "javascript" |
| 330: | } |
| 331: | if version_file_type in aliases: |
| 332: | version_file_type = aliases[version_file_type] |
| 333: | |
| 334: | f = codecs.open(version_file, 'r', 'utf-8') |
| 335: | content = f.read() |
| 336: | f.close() |
| 337: | |
| 338: | if not version_file_type: |
| 339: | # Guess the type. |
| 340: | base = basename(version_file) |
| 341: | ext = splitext(base)[1] |
| 342: | if ext == ".json": |
| 343: | version_file_type = "json" |
| 344: | elif ext == ".py": |
| 345: | version_file_type = "python" |
| 346: | elif ext == ".js": |
| 347: | version_file_type = "javascript" |
| 348: | elif content.startswith("#!"): |
| 349: | shebang = content.splitlines(False)[0] |
| 350: | shebang_bits = re.split(r'[/ \t]', shebang) |
| 351: | for name, typ in {"python": "python", "node": "javascript"}.items(): |
| 352: | if name in shebang_bits: |
| 353: | version_file_type = typ |
| 354: | break |
| 355: | elif base in ("VERSION", "VERSION.txt"): |
| 356: | version_file_type = "version" |
| 357: | if not version_file_type: |
| 358: | raise RuntimeError("can't extract version from '%s': no idea " |
| 359: | "what type of file it it" % version_file) |
| 360: | |
| 361: | if version_file_type == "json": |
| 362: | obj = json.loads(content) |
| 363: | version_info = _version_info_from_version(obj["version"]) |
| 364: | elif version_file_type == "python": |
| 365: | m = re.search(r'^__version_info__ = (.*?)$', content, re.M) |
| 366: | version_info = eval(m.group(1)) |
| 367: | elif version_file_type == "javascript": |
| 368: | m = re.search(r'^var VERSION = (\'|")(.*?)\1;$', content, re.M) |
| 369: | version_info = _version_info_from_version(m.group(2)) |
| 370: | elif version_file_type == "version": |
| 371: | version_info = _version_info_from_version(content.strip()) |
| 372: | else: |
| 373: | raise RuntimeError("unexpected version_file_type: %r" |
| 374: | % version_file_type) |
| 375: | return version_file_type, version_info |
| 376: | |
| 377: | |
| 378: | def parse_changelog(changes_path): |
| 379: | """Parse the given changelog path and return `(content, parsed, nyr)` |
| 380: | where `nyr` is the ' (not yet released)' marker and `parsed` looks like: |
| 381: | |
| 382: | [{'body': u'\n(nothing yet)\n\n', |
| 383: | 'verline': u'restify 1.0.1 (not yet released)', |
| 384: | 'version': u'1.0.1'}, # version is parsed out for top section only |
| 385: | {'body': u'...', |
| 386: | 'verline': u'1.0.0'}, |
| 387: | {'body': u'...', |
| 388: | 'verline': u'1.0.0-rc2'}, |
| 389: | {'body': u'...', |
| 390: | 'verline': u'1.0.0-rc1'}] |
| 391: | |
| 392: | A changelog (CHANGES.md) is expected to look like this: |
| 393: | |
| 394: | # $project Changelog |
| 395: | |
| 396: | ## $next_version (not yet released) |
| 397: | |
| 398: | ... |
| 399: | |
| 400: | ## $version1 |
| 401: | |
| 402: | ... |
| 403: | |
| 404: | ## $version2 |
| 405: | |
| 406: | ... and so on |
| 407: | |
| 408: | The version lines are enforced as follows: |
| 409: | |
| 410: | - The top entry should have a " (not yet released)" suffix. "Should" |
| 411: | because recovery from half-cutarelease failures is supported. |
| 412: | - A version string must be extractable from there, but it tries to |
| 413: | be loose (though strict "X.Y.Z" versioning is preferred). Allowed |
| 414: | |
| 415: | ## 1.0.0 |
| 416: | ## my project 1.0.1 |
| 417: | ## foo 1.2.3-rc2 |
| 418: | |
| 419: | Basically, (a) the " (not yet released)" is stripped, (b) the |
| 420: | last token is the version, and (c) that version must start with |
| 421: | a digit (sanity check). |
| 422: | """ |
| 423: | if not exists(changes_path): |
| 424: | raise Error("changelog file '%s' not found" % changes_path) |
| 425: | content = codecs.open(changes_path, 'r', 'utf-8').read() |
| 426: | |
| 427: | parser = re.compile( |
| 428: | r'^##\s*(?P<verline>[^\n]*?)\s*$(?P<body>.*?)(?=^##|\Z)', |
| 429: | re.M | re.S) |
| 430: | sections = parser.findall(content) |
| 431: | |
| 432: | # Sanity checks on changelog format. |
| 433: | if not sections: |
| 434: | template = "## 1.0.0 (not yet released)\n\n(nothing yet)\n" |
| 435: | raise Error("changelog '%s' must have at least one section, " |
| 436: | "suggestion:\n\n%s" % (changes_path, _indent(template))) |
| 437: | first_section_verline = sections[0][0] |
| 438: | nyr = ' (not yet released)' |
| 439: | #if not first_section_verline.endswith(nyr): |
| 440: | # eg = "## %s%s" % (first_section_verline, nyr) |
| 441: | # raise Error("changelog '%s' top section must end with %r, " |
| 442: | # "naive e.g.: '%s'" % (changes_path, nyr, eg)) |
| 443: | |
| 444: | items = [] |
| 445: | for i, section in enumerate(sections): |
| 446: | item = { |
| 447: | "verline": section[0], |
| 448: | "body": section[1] |
| 449: | } |
| 450: | if i == 0: |
| 451: | # We only bother to pull out 'version' for the top section. |
| 452: | verline = section[0] |
| 453: | if verline.endswith(nyr): |
| 454: | verline = verline[0:-len(nyr)] |
| 455: | version = verline.split()[-1] |
| 456: | try: |
| 457: | int(version[0]) |
| 458: | except ValueError: |
| 459: | msg = '' |
| 460: | if version.endswith(')'): |
| 461: | msg = " (cutarelease is picky about the trailing %r " \ |
| 462: | "on the top version line. Perhaps you misspelled " \ |
| 463: | "that?)" % nyr |
| 464: | raise Error("changelog '%s' top section version '%s' is " |
| 465: | "invalid: first char isn't a number%s" |
| 466: | % (changes_path, version, msg)) |
| 467: | item["version"] = version |
| 468: | items.append(item) |
| 469: | |
| 470: | return content, items, nyr |
| 471: | |
| 472: | ## {{{ http://code.activestate.com/recipes/577058/ (r2) |
| 473: | def query_yes_no(question, default="yes"): |
| 474: | """Ask a yes/no question via raw_input() and return their answer. |
| 475: | |
| 476: | "question" is a string that is presented to the user. |
| 477: | "default" is the presumed answer if the user just hits <Enter>. |
| 478: | It must be "yes" (the default), "no" or None (meaning |
| 479: | an answer is required of the user). |
| 480: | |
| 481: | The "answer" return value is one of "yes" or "no". |
| 482: | """ |
| 483: | valid = {"yes":"yes", "y":"yes", "ye":"yes", |
| 484: | "no":"no", "n":"no"} |
| 485: | if default == None: |
| 486: | prompt = " [y/n] " |
| 487: | elif default == "yes": |
| 488: | prompt = " [Y/n] " |
| 489: | elif default == "no": |
| 490: | prompt = " [y/N] " |
| 491: | else: |
| 492: | raise ValueError("invalid default answer: '%s'" % default) |
| 493: | |
| 494: | while 1: |
| 495: | sys.stdout.write(question + prompt) |
| 496: | choice = raw_input().lower() |
| 497: | if default is not None and choice == '': |
| 498: | return default |
| 499: | elif choice in valid.keys(): |
| 500: | return valid[choice] |
| 501: | else: |
| 502: | sys.stdout.write("Please respond with 'yes' or 'no' "\ |
| 503: | "(or 'y' or 'n').\n") |
| 504: | ## end of http://code.activestate.com/recipes/577058/ }}} |
| 505: | |
| 506: | def _capture_stdout(argv): |
| 507: | import subprocess |
| 508: | p = subprocess.Popen(argv, stdout=subprocess.PIPE) |
| 509: | return p.communicate()[0] |
| 510: | |
| 511: | class _NoReflowFormatter(optparse.IndentedHelpFormatter): |
| 512: | """An optparse formatter that does NOT reflow the description.""" |
| 513: | def format_description(self, description): |
| 514: | return description or "" |
| 515: | |
| 516: | def run(cmd): |
| 517: | """Run the given command. |
| 518: | |
| 519: | Raises OSError is the command returns a non-zero exit status. |
| 520: | """ |
| 521: | log.debug("running '%s'", cmd) |
| 522: | fixed_cmd = cmd |
| 523: | if sys.platform == "win32" and cmd.count('"') > 2: |
| 524: | fixed_cmd = '"' + cmd + '"' |
| 525: | retval = os.system(fixed_cmd) |
| 526: | if hasattr(os, "WEXITSTATUS"): |
| 527: | status = os.WEXITSTATUS(retval) |
| 528: | else: |
| 529: | status = retval |
| 530: | if status: |
| 531: | raise OSError(status, "error running '%s'" % cmd) |
| 532: | |
| 533: | def _setup_command_prefix(): |
| 534: | prefix = "" |
| 535: | if sys.platform == "darwin": |
| 536: | # http://forums.macosxhints.com/archive/index.php/t-43243.html |
| 537: | # This is an Apple customization to `tar` to avoid creating |
| 538: | # '._foo' files for extended-attributes for archived files. |
| 539: | prefix = "COPY_EXTENDED_ATTRIBUTES_DISABLE=1 " |
| 540: | return prefix |
| 541: | |
| 542: | |
| 543: | #---- mainline |
| 544: | |
| 545: | def main(argv): |
| 546: | logging.basicConfig(format="%(name)s: %(levelname)s: %(message)s") |
| 547: | log.setLevel(logging.INFO) |
| 548: | |
| 549: | # Parse options. |
| 550: | parser = optparse.OptionParser(prog="cutarelease", usage='', |
| 551: | version="%prog " + __version__, description=__doc__, |
| 552: | formatter=_NoReflowFormatter()) |
| 553: | parser.add_option("-v", "--verbose", dest="log_level", |
| 554: | action="store_const", const=logging.DEBUG, |
| 555: | help="more verbose output") |
| 556: | parser.add_option("-q", "--quiet", dest="log_level", |
| 557: | action="store_const", const=logging.WARNING, |
| 558: | help="quieter output (just warnings and errors)") |
| 559: | parser.set_default("log_level", logging.INFO) |
| 560: | parser.add_option("--test", action="store_true", |
| 561: | help="run self-test and exit (use 'eol.py -v --test' for verbose test output)") |
| 562: | parser.add_option("-p", "--project-name", metavar="NAME", |
| 563: | help='the name of this project (default is the base dir name)', |
| 564: | default=basename(os.getcwd())) |
| 565: | parser.add_option("-f", "--version-file", metavar="[TYPE:]PATH", |
| 566: | action='append', dest="version_files", |
| 567: | help='The path to the project file holding the version info. Can be ' |
| 568: | 'specified multiple times if more than one file should be updated ' |
| 569: | 'with new version info. If excluded, it will be guessed.') |
| 570: | parser.add_option("-n", "--dry-run", action="store_true", |
| 571: | help='Do a dry-run', default=False) |
| 572: | opts, args = parser.parse_args() |
| 573: | log.setLevel(opts.log_level) |
| 574: | |
| 575: | cutarelease(opts.project_name, opts.version_files, dry_run=opts.dry_run) |
| 576: | |
| 577: | |
| 578: | ## {{{ http://code.activestate.com/recipes/577258/ (r5+) |
| 579: | if __name__ == "__main__": |
| 580: | try: |
| 581: | retval = main(sys.argv) |
| 582: | except KeyboardInterrupt: |
| 583: | sys.exit(1) |
| 584: | except SystemExit: |
| 585: | raise |
| 586: | except: |
| 587: | import traceback, logging |
| 588: | if not log.handlers and not logging.root.handlers: |
| 589: | logging.basicConfig() |
| 590: | skip_it = False |
| 591: | exc_info = sys.exc_info() |
| 592: | if hasattr(exc_info[0], "__name__"): |
| 593: | exc_class, exc, tb = exc_info |
| 594: | if isinstance(exc, IOError) and exc.args[0] == 32: |
| 595: | # Skip 'IOError: [Errno 32] Broken pipe': often a cancelling of `less`. |
| 596: | skip_it = True |
| 597: | if not skip_it: |
| 598: | tb_path, tb_lineno, tb_func = traceback.extract_tb(tb)[-1][:3] |
| 599: | log.error("%s (%s:%s in %s)", exc_info[1], tb_path, |
| 600: | tb_lineno, tb_func) |
| 601: | else: # string exception |
| 602: | log.error(exc_info[0]) |
| 603: | if not skip_it: |
| 604: | if log.isEnabledFor(logging.DEBUG): |
| 605: | traceback.print_exception(*exc_info) |
| 606: | sys.exit(1) |
| 607: | else: |
| 608: | sys.exit(retval) |
| 609: | ## end of http://code.activestate.com/recipes/577258/ }}} |
