1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
|
# -*- coding: UTF-8 -*-
"""
Tasks for releasing this project.
Normal steps::
python setup.py sdist bdist_wheel
twine register dist/{project}-{version}.tar.gz
twine upload dist/*
twine upload --skip-existing dist/*
python setup.py upload
# -- DEPRECATED: No longer supported -> Use RTD instead
# -- DEPRECATED: python setup.py upload_docs
pypi repositories:
* https://pypi.python.org/pypi
* https://testpypi.python.org/pypi (not working anymore)
* https://test.pypi.org/legacy/ (not working anymore)
Configuration file for pypi repositories:
.. code-block:: init
# -- FILE: $HOME/.pypirc
[distutils]
index-servers =
pypi
testpypi
[pypi]
# DEPRECATED: repository = https://pypi.python.org/pypi
username = __USERNAME_HERE__
password:
[testpypi]
# DEPRECATED: repository = https://test.pypi.org/legacy
username = __USERNAME_HERE__
password:
.. seealso::
* https://packaging.python.org/
* https://packaging.python.org/guides/
* https://packaging.python.org/tutorials/distributing-packages/
"""
from __future__ import absolute_import, print_function
from invoke import Collection, task
from invoke_cleanup import path_glob
from .invoke_dry_run import DryRunContext
# -----------------------------------------------------------------------------
# TASKS:
# -----------------------------------------------------------------------------
@task
def checklist(ctx=None): # pylint: disable=unused-argument
"""Checklist for releasing this project."""
checklist_text = """PRE-RELEASE CHECKLIST:
[ ] Everything is checked in
[ ] All tests pass w/ tox
RELEASE CHECKLIST:
[{x1}] Bump version to new-version by adding tag to the repository
[{x2}] Build packages (sdist, bdist_wheel via prepare)
[{x3}] Register and upload packages to testpypi repository (first)
[{x4}] Verify release is OK and packages from testpypi are usable
[{x5}] Register and upload packages to pypi repository
[{x6}] Push last changes to Github repository
POST-RELEASE CHECKLIST:
[ ] Bump version to new-develop-version by adding tag to the repository
[ ] Adapt CHANGES (if necessary)
[ ] Commit latest changes to Github repository
"""
steps = dict(x1=None, x2=None, x3=None, x4=None, x5=None, x6=None)
yesno_map = {True: "x", False: "_", None: " "}
answers = {name: yesno_map[value]
for name, value in steps.items()}
print(checklist_text.format(**answers))
@task(name="bump_version")
def bump_version(ctx, new_version, dry_run=False):
"""Bump version (to prepare a new release)."""
if not new_version.startswith("v"):
new_version = "v{version}".format(version=new_version)
if dry_run:
ctx = DryRunContext(ctx)
ctx.run("git tag {version}".format(version=new_version))
@task(name="build", aliases=["build_packages"])
def build_packages(ctx, hide=False):
"""Build packages for this release."""
print("build_packages:")
ctx.run("python -m build", echo=True, hide=hide)
@task
def prepare(ctx, new_version=None, hide=True,
dry_run=False):
"""Prepare the release: bump version, build packages, ..."""
if new_version is not None:
bump_version(ctx, new_version, dry_run=dry_run)
build_packages(ctx, hide=hide)
packages = ensure_packages_exist(ctx, check_only=True)
print_packages(packages)
# -- NOT-NEEDED:
# @task(name="register")
# def register_packages(ctx, repo=None, dry_run=False):
# """Register release (packages) in artifact-store/repository."""
# original_ctx = ctx
# if repo is None:
# repo = ctx.project.repo or "pypi"
# if dry_run:
# ctx = DryRunContext(ctx)
# packages = ensure_packages_exist(original_ctx)
# print_packages(packages)
# for artifact in packages:
# ctx.run("twine register --repository={repo} {artifact}".format(
# artifact=artifact, repo=repo))
@task
def upload(ctx, repo=None, repo_url=None, dry_run=False,
skip_existing=False, verbose=False):
"""Upload release packages to repository (artifact-store)."""
if repo is None:
repo = ctx.project.repo or "pypi"
if repo_url is None:
repo_url = ctx.project.repo_url or None
original_ctx = ctx
if dry_run:
ctx = DryRunContext(ctx)
# -- OPTIONS:
opts = []
if repo_url:
opts.append("--repository-url={0}".format(repo_url))
elif repo:
opts.append("--repository={0}".format(repo))
if skip_existing:
opts.append("--skip-existing")
if verbose:
opts.append("--verbose")
packages = ensure_packages_exist(original_ctx)
print_packages(packages)
ctx.run("twine upload {opts} dist/*".format(opts=" ".join(opts)))
# ctx.run("twine upload --repository={repo} dist/*".format(repo=repo))
# 2018-05-05 WORK-AROUND for new https://pypi.org/:
# twine upload --repository-url=https://upload.pypi.org/legacy /dist/*
# NOT-WORKING: repo_url = "https://upload.pypi.org/simple/"
#
# ctx.run("twine upload --repository-url={repo_url} {opts} dist/*".format(
# repo_url=repo_url, opts=" ".join(opts)))
# ctx.run("twine upload --repository={repo} {opts} dist/*".format(
# repo=repo, opts=" ".join(opts)))
# -- DEPRECATED: Use RTD instead
# @task(name="upload_docs")
# def upload_docs(ctx, repo=None, dry_run=False):
# """Upload and publish docs.
#
# NOTE: Docs are built first.
# """
# if repo is None:
# repo = ctx.project.repo or "pypi"
# if dry_run:
# ctx = DryRunContext(ctx)
#
# ctx.run("python setup.py upload_docs")
#
# -----------------------------------------------------------------------------
# TASK HELPERS:
# -----------------------------------------------------------------------------
def print_packages(packages):
print("PACKAGES[%d]:" % len(packages))
for package in packages:
package_size = package.stat().st_size
package_time = package.stat().st_mtime
print(" - %s (size=%s)" % (package, package_size))
def ensure_packages_exist(ctx, pattern=None, check_only=False):
if pattern is None:
project_name = ctx.project.name
project_prefix = project_name.replace("_", "-").split("-")[0]
pattern = "dist/%s*" % project_prefix
packages = list(path_glob(pattern, current_dir="."))
if not packages:
if check_only:
message = "No artifacts found: pattern=%s" % pattern
raise RuntimeError(message)
else:
# -- RECURSIVE-SELF-CALL: Once
print("NO-PACKAGES-FOUND: Build packages first ...")
build_packages(ctx, hide=True)
packages = ensure_packages_exist(ctx, pattern,
check_only=True)
return packages
# -----------------------------------------------------------------------------
# TASK CONFIGURATION:
# -----------------------------------------------------------------------------
# DISABLED: register_packages
namespace = Collection(bump_version, checklist, prepare, build_packages, upload)
namespace.configure({
"project": {
"repo": "pypi",
"repo_url": None,
}
})
|