Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
300 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
|
||
# This is a Makefile for the `mk` tool. (Limited) details for that here: | ||
# <http://svn.openkomodo.com/openkomodo/browse/mk> | ||
|
||
import sys | ||
import os | ||
from os.path import join, dirname, normpath, abspath, exists, basename, expanduser | ||
import re | ||
from glob import glob | ||
import codecs | ||
import webbrowser | ||
|
||
import mklib | ||
assert mklib.__version_info__ >= (0,7,2) # for `mklib.mk` | ||
from mklib.common import MkError | ||
from mklib import Task, mk | ||
from mklib import sh | ||
|
||
|
||
class bugs(Task): | ||
"""open bug/issues page""" | ||
def make(self): | ||
webbrowser.open("http://github.com/ActiveState/appdirs/issues") | ||
|
||
class site(Task): | ||
"""open project page""" | ||
def make(self): | ||
webbrowser.open("http://github.com/ActiveState/appdirs") | ||
|
||
class pypi(Task): | ||
"""open project page""" | ||
def make(self): | ||
webbrowser.open("http://pypi.python.org/pypi/appdirs/") | ||
|
||
class cut_a_release(Task): | ||
"""automate the steps for cutting a release | ||
See <http://github.com/trentm/eol/blob/master/docs/devguide.md> | ||
for details. | ||
""" | ||
proj_name = "appdirs" | ||
version_py_path = "lib/appdirs.py" | ||
version_module = "appdirs" | ||
_changes_parser = re.compile(r'^## %s (?P<ver>[\d\.abc]+)' | ||
r'(?P<nyr>\s+\(not yet released\))?' | ||
r'(?P<body>.*?)(?=^##|\Z)' % proj_name, re.M | re.S) | ||
|
||
def make(self): | ||
DRY_RUN = False | ||
version = self._get_version() | ||
|
||
# Confirm | ||
if not DRY_RUN: | ||
answer = query_yes_no("* * *\n" | ||
"Are you sure you want cut a %s release?\n" | ||
"This will involved commits and a release to pypi." % version, | ||
default="no") | ||
if answer != "yes": | ||
self.log.info("user abort") | ||
return | ||
print "* * *" | ||
self.log.info("cutting a %s release", version) | ||
|
||
# Checks: Ensure there is a section in changes for this version. | ||
changes_path = join(self.dir, "CHANGES.md") | ||
changes_txt = changes_txt_before = codecs.open(changes_path, 'r', 'utf-8').read() | ||
changes_sections = self._changes_parser.findall(changes_txt) | ||
top_ver = changes_sections[0][0] | ||
if top_ver != version: | ||
raise MkError("top section in `CHANGES.md' is for " | ||
"version %r, expected version %r: aborting" | ||
% (top_ver, version)) | ||
top_nyr = changes_sections[0][1] | ||
if not top_nyr: | ||
answer = query_yes_no("\n* * *\n" | ||
"The top section in `CHANGES.md' doesn't have the expected\n" | ||
"'(not yet released)' marker. Has this been released already?", | ||
default="yes") | ||
if answer != "no": | ||
self.log.info("abort") | ||
return | ||
print "* * *" | ||
top_body = changes_sections[0][2] | ||
if top_body.strip() == "(nothing yet)": | ||
raise MkError("top section body is `(nothing yet)': it looks like " | ||
"nothing has been added to this release") | ||
|
||
# Commits to prepare release. | ||
changes_txt = changes_txt.replace(" (not yet released)", "", 1) | ||
if not DRY_RUN and changes_txt != changes_txt_before: | ||
self.log.info("prepare `CHANGES.md' for release") | ||
f = codecs.open(changes_path, 'w', 'utf-8') | ||
f.write(changes_txt) | ||
f.close() | ||
sh.run('git commit %s -m "prepare for %s release"' | ||
% (changes_path, version), self.log.debug) | ||
|
||
# Tag version and push. | ||
curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t) | ||
if not DRY_RUN and version not in curr_tags: | ||
self.log.info("tag the release") | ||
sh.run('git tag -a "%s" -m "version %s"' % (version, version), | ||
self.log.debug) | ||
sh.run('git push --tags', self.log.debug) | ||
|
||
# Release to PyPI. | ||
self.log.info("release to pypi") | ||
if not DRY_RUN: | ||
mk("pypi_upload") | ||
|
||
# Commits to prepare for future dev and push. | ||
next_version = self._get_next_version(version) | ||
self.log.info("prepare for future dev (version %s)", next_version) | ||
marker = "## %s %s\n" % (self.proj_name, version) | ||
if marker not in changes_txt: | ||
raise MkError("couldn't find `%s' marker in `%s' " | ||
"content: can't prep for subsequent dev" % (marker, changes_path)) | ||
changes_txt = changes_txt.replace("## %s %s\n" % (self.proj_name, version), | ||
"## %s %s (not yet released)\n\n(nothing yet)\n\n## %s %s\n" % ( | ||
self.proj_name, next_version, self.proj_name, version)) | ||
if not DRY_RUN: | ||
f = codecs.open(changes_path, 'w', 'utf-8') | ||
f.write(changes_txt) | ||
f.close() | ||
|
||
ver_path = join(self.dir, normpath(self.version_py_path)) | ||
ver_content = codecs.open(ver_path, 'r', 'utf-8').read() | ||
version_tuple = self._tuple_from_version(version) | ||
next_version_tuple = self._tuple_from_version(next_version) | ||
marker = "__version_info__ = %r" % (version_tuple,) | ||
if marker not in ver_content: | ||
raise MkError("couldn't find `%s' version marker in `%s' " | ||
"content: can't prep for subsequent dev" % (marker, ver_path)) | ||
ver_content = ver_content.replace(marker, | ||
"__version_info__ = %r" % (next_version_tuple,)) | ||
if not DRY_RUN: | ||
f = codecs.open(ver_path, 'w', 'utf-8') | ||
f.write(ver_content) | ||
f.close() | ||
|
||
if not DRY_RUN: | ||
sh.run('git commit %s %s -m "prep for future dev"' % ( | ||
changes_path, ver_path)) | ||
sh.run('git push') | ||
|
||
def _tuple_from_version(self, version): | ||
def _intify(s): | ||
try: | ||
return int(s) | ||
except ValueError: | ||
return s | ||
return tuple(_intify(b) for b in version.split('.')) | ||
|
||
def _get_next_version(self, version): | ||
last_bit = version.rsplit('.', 1)[-1] | ||
try: | ||
last_bit = int(last_bit) | ||
except ValueError: # e.g. "1a2" | ||
last_bit = int(re.split('[abc]', last_bit, 1)[-1]) | ||
return version[:-len(str(last_bit))] + str(last_bit + 1) | ||
|
||
def _get_version(self): | ||
lib_dir = join(dirname(abspath(__file__)), "lib") | ||
sys.path.insert(0, lib_dir) | ||
try: | ||
mod = __import__(self.version_module) | ||
return mod.__version__ | ||
finally: | ||
del sys.path[0] | ||
|
||
|
||
class clean(Task): | ||
"""Clean generated files and dirs.""" | ||
def make(self): | ||
patterns = [ | ||
"dist", | ||
"build", | ||
"MANIFEST", | ||
"*.pyc", | ||
"lib/*.pyc", | ||
] | ||
for pattern in patterns: | ||
p = join(self.dir, pattern) | ||
for path in glob(p): | ||
sh.rm(path, log=self.log) | ||
|
||
class sdist(Task): | ||
"""python setup.py sdist""" | ||
def make(self): | ||
sh.run_in_dir("%spython setup.py sdist --formats zip" | ||
% _setup_command_prefix(), | ||
self.dir, self.log.debug) | ||
|
||
class pypi_upload(Task): | ||
"""Upload release to pypi.""" | ||
def make(self): | ||
sh.run_in_dir("%spython setup.py sdist --formats zip upload" | ||
% _setup_command_prefix(), | ||
self.dir, self.log.debug) | ||
|
||
sys.path.insert(0, join(self.dir, "lib")) | ||
url = "http://pypi.python.org/pypi/appdirs/" | ||
import webbrowser | ||
webbrowser.open_new(url) | ||
|
||
class test(Task): | ||
"""Run all tests (except known failures).""" | ||
def make(self): | ||
for ver, python in self._gen_pythons(): | ||
if ver < (2,3): | ||
# Don't support Python < 2.3. | ||
continue | ||
#elif ver >= (3, 0): | ||
# # Don't yet support Python 3. | ||
# continue | ||
ver_str = "%s.%s" % ver | ||
print "-- test with Python %s (%s)" % (ver_str, python) | ||
assert ' ' not in python | ||
sh.run_in_dir("%s test.py -- -knownfailure" % python, | ||
join(self.dir, "test")) | ||
|
||
def _python_ver_from_python(self, python): | ||
assert ' ' not in python | ||
o = os.popen('''%s -c "import sys; print(sys.version)"''' % python) | ||
ver_str = o.read().strip() | ||
ver_bits = re.split("\.|[^\d]", ver_str, 2)[:2] | ||
ver = tuple(map(int, ver_bits)) | ||
return ver | ||
|
||
def _gen_python_names(self): | ||
yield "python" | ||
for ver in [(2,4), (2,5), (2,6), (2,7), (3,0), (3,1)]: | ||
yield "python%d.%d" % ver | ||
if sys.platform == "win32": | ||
yield "python%d%d" % ver | ||
|
||
def _gen_pythons(self): | ||
import which # `pypm|pip install which` | ||
python_from_ver = {} | ||
for name in self._gen_python_names(): | ||
for python in which.whichall(name): | ||
ver = self._python_ver_from_python(python) | ||
if ver not in python_from_ver: | ||
python_from_ver[ver] = python | ||
for ver, python in sorted(python_from_ver.items()): | ||
yield ver, python | ||
|
||
|
||
|
||
|
||
#---- internal support stuff | ||
|
||
## {{{ http://code.activestate.com/recipes/577058/ (r2) | ||
def query_yes_no(question, default="yes"): | ||
"""Ask a yes/no question via raw_input() and return their answer. | ||
"question" is a string that is presented to the user. | ||
"default" is the presumed answer if the user just hits <Enter>. | ||
It must be "yes" (the default), "no" or None (meaning | ||
an answer is required of the user). | ||
The "answer" return value is one of "yes" or "no". | ||
""" | ||
valid = {"yes":"yes", "y":"yes", "ye":"yes", | ||
"no":"no", "n":"no"} | ||
if default == None: | ||
prompt = " [y/n] " | ||
elif default == "yes": | ||
prompt = " [Y/n] " | ||
elif default == "no": | ||
prompt = " [y/N] " | ||
else: | ||
raise ValueError("invalid default answer: '%s'" % default) | ||
|
||
while 1: | ||
sys.stdout.write(question + prompt) | ||
choice = raw_input().lower() | ||
if default is not None and choice == '': | ||
return default | ||
elif choice in valid.keys(): | ||
return valid[choice] | ||
else: | ||
sys.stdout.write("Please respond with 'yes' or 'no' "\ | ||
"(or 'y' or 'n').\n") | ||
## end of http://code.activestate.com/recipes/577058/ }}} | ||
|
||
|
||
def _setup_command_prefix(): | ||
prefix = "" | ||
if sys.platform == "darwin": | ||
# http://forums.macosxhints.com/archive/index.php/t-43243.html | ||
# This is an Apple customization to `tar` to avoid creating | ||
# '._foo' files for extended-attributes for archived files. | ||
prefix = "COPY_EXTENDED_ATTRIBUTES_DISABLE=1 " | ||
return prefix | ||
|
||
def _capture_stdout(argv): | ||
import subprocess | ||
p = subprocess.Popen(argv, stdout=subprocess.PIPE) | ||
return p.communicate()[0] |