forked from python/mypy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add script that builds packages and uploads them to PyPI (python#2948)
- Loading branch information
1 parent
8504a19
commit 64dd2e7
Showing
1 changed file
with
160 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,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() |