Skip to content

Commit

Permalink
Add script that builds packages and uploads them to PyPI (python#2948)
Browse files Browse the repository at this point in the history
  • Loading branch information
JukkaL authored and gvanrossum committed Mar 6, 2017
1 parent 8504a19 commit 64dd2e7
Showing 1 changed file with 160 additions and 0 deletions.
160 changes: 160 additions & 0 deletions misc/upload-pypi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Build and upload mypy packages for Linux and macOS to PyPI.
Note: This should be run on macOS using offical python.org Python 3.6 or
later, as this is the only tested configuration. Use --force to
run anyway.
This uses a fresh repo clone and a fresh virtualenv to avoid depending on
local state.
Ideas for improvements:
- also upload Windows wheels
- try installing the generated packages and running mypy
- try installing the uploaded packages and running mypy
- run tests
- verify that there is a green travis build
"""

import argparse
import getpass
import os
import os.path
import re
import subprocess
import sys
import tempfile
from typing import Any


class Builder:
def __init__(self, version: str, force: bool, no_upload: bool) -> None:
if not re.match(r'0\.[0-9]{3}$', version):
sys.exit('Invalid version {!r} (expected form 0.123)'.format(version))
self.version = version
self.force = force
self.no_upload = no_upload
self.target_dir = tempfile.mkdtemp()
self.repo_dir = os.path.join(self.target_dir, 'mypy')

def build_and_upload(self) -> None:
self.prompt()
self.run_sanity_checks()
print('Temporary target directory: {}'.format(self.target_dir))
self.git_clone_repo()
self.git_check_out_tag()
self.verify_version()
self.make_virtualenv()
self.install_dependencies()
self.make_wheel()
self.make_sdist()
if not self.no_upload:
self.upload_wheel()
self.upload_sdist()
self.heading('Successfully uploaded wheel and sdist for mypy {}'.format(self.version))
print("<< Don't forget to upload Windows wheels! >>")
else:
self.heading('Successfully built wheel and sdist for mypy {}'.format(self.version))
dist_dir = os.path.join(self.repo_dir, 'dist')
print('Generated packages:'.format(dist_dir))
for fnam in sorted(os.listdir(dist_dir)):
print(' {}'.format(os.path.join(dist_dir, fnam)))

def prompt(self) -> None:
if self.force:
return
extra = '' if self.no_upload else ' and upload'
print('This will build{} PyPI packages for mypy {}.'.format(extra, self.version))
response = input('Proceeed? [yN] '.format(self.version))
if response.lower() != 'y':
sys.exit('Exiting')

def verify_version(self) -> None:
version_path = os.path.join(self.repo_dir, 'mypy', 'version.py')
with open(version_path) as f:
contents = f.read()
if "'{}'".format(self.version) not in contents:
sys.stderr.write(
'\nError: Version {} does not match {}/mypy/version.py\n'.format(
self.version, self.repo_dir))
sys.exit(2)

def run_sanity_checks(self) -> None:
if not sys.version_info >= (3, 6):
sys.exit('You must use Python 3.6 or later to build mypy')
if sys.platform != 'darwin' and not self.force:
sys.exit('You should run this on macOS; use --force to go ahead anyway')
os_file = os.path.realpath(os.__file__)
if not os_file.startswith('/Library/Frameworks') and not self.force:
# Be defensive -- Python from brew may produce bad packages, for example.
sys.exit('Error -- run this script using an official Python build from python.org')
if getpass.getuser() == 'root':
sys.exit('This script must not be run as root')

def git_clone_repo(self) -> None:
self.heading('Cloning mypy git repository')
self.run('git clone https://github.com/python/mypy')

def git_check_out_tag(self) -> None:
tag = 'v{}'.format(self.version)
self.heading('Check out {}'.format(tag))
self.run('cd mypy && git checkout {}'.format(tag))
self.run('cd mypy && git submodule update --init typeshed'.format(tag))

def make_virtualenv(self) -> None:
self.heading('Creating a fresh virtualenv')
self.run('virtualenv -p {} mypy-venv'.format(sys.executable))

def install_dependencies(self) -> None:
self.heading('Installing build dependencies')
self.run_in_virtualenv('pip3 install wheel twine && pip3 install -U setuptools')

def make_wheel(self) -> None:
self.heading('Building wheel')
self.run_in_virtualenv('python3 setup.py bdist_wheel')

def make_sdist(self) -> None:
self.heading('Building sdist')
self.run_in_virtualenv('python3 setup.py sdist')

def upload_wheel(self) -> None:
self.heading('Uploading wheel')
self.run_in_virtualenv('twine upload dist/mypy-{}-py3-none-any.whl'.format(self.version))

def upload_sdist(self) -> None:
self.heading('Uploading sdist')
self.run_in_virtualenv('twine upload dist/mypy-{}.tar.gz'.format(self.version))

def run(self, cmd: str) -> None:
try:
subprocess.check_call(cmd, shell=True, cwd=self.target_dir)
except subprocess.CalledProcessError:
sys.stderr.write('Error: Command {!r} failed\n'.format(cmd))
sys.exit(1)

def run_in_virtualenv(self, cmd: str) -> None:
self.run('source mypy-venv/bin/activate && cd mypy &&' + cmd)

def heading(self, heading: str) -> None:
print()
print('==== {} ===='.format(heading))
print()


def parse_args() -> Any:
parser = argparse.ArgumentParser(
description='PyPI mypy package uploader (for non-Windows packages only)')
parser.add_argument('--force', action='store_true', default=False,
help='Skip prompts and sanity checks (be careful!)')
parser.add_argument('--no-upload', action='store_true', default=False,
help="Only build packages but don't upload")
parser.add_argument('version', help='Mypy version to release')
return parser.parse_args()


if __name__ == '__main__':
args = parse_args()
builder = Builder(args.version, args.force, args.no_upload)
builder.build_and_upload()

0 comments on commit 64dd2e7

Please sign in to comment.