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