aboutsummaryrefslogtreecommitdiff
path: root/tasks/release.py
blob: bea347ec71c5ca608518755696b369d04748f3fd (plain)
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 ._tasklet_cleanup import path_glob
from ._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 and tag repository (via bump_version)
[{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 (via bump_version)
[ ]  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, version_part=None, dry_run=False):
    """Bump version (to prepare a new release)."""
    version_part = version_part or "minor"
    if dry_run:
        ctx = DryRunContext(ctx)
    ctx.run("bumpversion --new-version={} {}".format(new_version,
                                                     version_part))


@task(name="build", aliases=["build_packages"])
def build_packages(ctx, hide=False):
    """Build packages for this release."""
    print("build_packages:")
    ctx.run("python setup.py sdist bdist_wheel", echo=True, hide=hide)


@task
def prepare(ctx, new_version=None, version_part=None, hide=True,
            dry_run=False):
    """Prepare the release: bump version, build packages, ..."""
    if new_version is not None:
        bump_version(ctx, new_version, version_part=version_part,
                     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,
    }
})