1. PyPA
  2. Python Packaging Authority Projects
  3. setuptools


setuptools / release.py

The default branch has multiple heads

#!/usr/bin/env python

Script to fully automate the release process. Requires Python 2.6+
with sphinx installed and the 'hg' command on the path.

from __future__ import print_function

import subprocess
import shutil
import os
import sys
import getpass
import collections
import itertools
import re

import requests

	input = raw_input
except NameError:

	zip_longest = itertools.zip_longest
except AttributeError:
	zip_longest = itertools.izip_longest

	import keyring
except Exception:

VERSION = '0.9'
PACKAGE_INDEX = 'https://pypi.python.org/pypi'

def set_versions():
	global VERSION
	version = input("Release as version [%s]> " % VERSION) or VERSION
	if version != VERSION:
		VERSION = bump_versions(version)

def infer_next_version(version):
	Infer a next version from the current version by incrementing the last
	number or appending a number.

	>>> infer_next_version('1.0')

	>>> infer_next_version('1.0b')

	>>> infer_next_version('1.0.9')

	>>> infer_next_version('1')

	>>> infer_next_version('')
	def incr(match):
		ver = int(match.group(0) or '0')
		return str(ver + 1)
	return re.sub('\d*$', incr, version)

files_with_versions = (
	'docs/conf.py', 'setup.py', 'release.py', 'ez_setup.py',

def get_repo_name():
	Get the repo name from the hgrc default path.
	default = subprocess.check_output('hg paths default').strip().decode('utf-8')
	parts = default.split('/')
	if parts[-1] == '':
	return '/'.join(parts[-2:])

def get_mercurial_creds(system='https://bitbucket.org', username=None):
	Return named tuple of username,password in much the same way that
	Mercurial would (from the keyring).
	# todo: consider getting this from .hgrc
	username = username or getpass.getuser()
	keyring_username = '@@'.join((username, system))
	system = 'Mercurial'
	password = (
		keyring.get_password(system, keyring_username)
		if 'keyring' in globals()
		else None
	if not password:
		password = getpass.getpass()
	Credential = collections.namedtuple('Credential', 'username password')
	return Credential(username, password)

def add_milestone_and_version(version):
	base = 'https://api.bitbucket.org'
	for type in 'milestones', 'versions':
		url = (base + '/1.0/repositories/{repo}/issues/{type}'
			.format(repo = get_repo_name(), type=type))
		resp = requests.post(url=url,
			data='name='+version, auth=get_mercurial_creds())

def bump_versions(target_ver):
	for filename in files_with_versions:
		bump_version(filename, target_ver)
	subprocess.check_call(['hg', 'ci', '-m',
		'Bumped to {target_ver} in preparation for next '
	return target_ver

def bump_version(filename, target_ver):
	with open(filename, 'rb') as f:
		lines = [
			line.replace(VERSION.encode('ascii'), target_ver.encode('ascii'))
			for line in f
	with open(filename, 'wb') as f:

def do_release():
	assert all(map(os.path.exists, files_with_versions)), (
		"Expected file(s) missing")

	assert has_sphinx(), "You must have Sphinx installed to release"


	res = input('Have you read through the SCM changelog and '
		'confirmed the changelog is current for releasing {VERSION}? '
	if not res.lower().startswith('y'):
		print("Please do that")
		raise SystemExit(1)

	print("Travis-CI tests: http://travis-ci.org/#!/jaraco/setuptools")
	res = input('Have you or has someone verified that the tests '
		'pass on this revision? ')
	if not res.lower().startswith('y'):
		print("Please do that")
		raise SystemExit(2)

	subprocess.check_call(['hg', 'tag', VERSION])

	subprocess.check_call(['hg', 'update', VERSION])


	# update to the tip for the next operation
	subprocess.check_call(['hg', 'update'])

	# we just tagged the current version, bump for the next release.
	next_ver = bump_versions(infer_next_version(VERSION))

	# push the changes
	subprocess.check_call(['hg', 'push'])


def upload_to_pypi():
	linkify('CHANGES.txt', 'CHANGES (links).txt')

	has_docs = build_docs()
	if os.path.isdir('./dist'):
	cmd = [
		sys.executable, 'setup.py', '-q',
		'egg_info', '-RD', '-b', '',
		'register', '-r', PACKAGE_INDEX,
		'upload', '-r', PACKAGE_INDEX,
	if has_docs:
			'upload_docs', '-r', PACKAGE_INDEX
	env = os.environ.copy()
	subprocess.check_call(cmd, env=env)

def has_sphinx():
		devnull = open(os.path.devnull, 'wb')
		subprocess.Popen(['sphinx-build', '--version'], stdout=devnull,
	except Exception:
		return False
	return True

def build_docs():
	if not os.path.isdir('docs'):
	if os.path.isdir('docs/build'):
	cmd = [
		'-b', 'html',
		'-d', 'build/doctrees',
	subprocess.check_call(cmd, cwd='docs')
	return True

def linkify(source, dest):
	with open(source) as source:
		out = _linkified_text(source.read())
	with open(dest, 'w') as dest:

def _linkified(rst_path):
	"return contents of reStructureText file with linked issue references"
	rst_file = open(rst_path)
	rst_content = rst_file.read()

	return _linkified_text(rst_content)

def _linkified_text(rst_content):
	# first identify any existing HREFs so they're not changed
	HREF_pattern = re.compile('`.*?`_', re.MULTILINE | re.DOTALL)

	# split on the HREF pattern, returning the parts to be linkified
	plain_text_parts = HREF_pattern.split(rst_content)
	anchors = []
	linkified_parts = [_linkified_part(part, anchors)
		for part in plain_text_parts]
	pairs = zip_longest(
	rst_content = ''.join(flatten(pairs))

	anchors = sorted(anchors)

	bitroot = 'https://bitbucket.org/tarek/distribute'
	rst_content += "\n"
	for x in anchors:
		issue = re.findall(r'\d+', x)[0]
		rst_content += '.. _`%s`: %s/issue/%s\n' % (x, bitroot, issue)
	rst_content += "\n"
	return rst_content

def flatten(listOfLists):
	"Flatten one level of nesting"
	return itertools.chain.from_iterable(listOfLists)

def _linkified_part(text, anchors):
	Linkify a part and collect any anchors generated
	revision = re.compile(r'\b(issue\s+#?\d+)\b', re.M | re.I)

	anchors.extend(revision.findall(text)) # ['Issue #43', ...]
	return revision.sub(r'`\1`_', text)

if __name__ == '__main__':