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, repo_url=None, dry_run=False,
135           skip_existing=False, verbose=False):
136    """Upload release packages to repository (artifact-store)."""
137    if repo is None:
138        repo = ctx.project.repo or "pypi"
139    if repo_url is None:
140        repo_url = ctx.project.repo_url or None
141    original_ctx = ctx
142    if dry_run:
143        ctx = DryRunContext(ctx)
144
145    # -- OPTIONS:
146    opts = []
147    if repo_url:
148        opts.append("--repository-url={0}".format(repo_url))
149    elif repo:
150        opts.append("--repository={0}".format(repo))
151    if skip_existing:
152        opts.append("--skip-existing")
153    if verbose:
154        opts.append("--verbose")
155
156    packages = ensure_packages_exist(original_ctx)
157    print_packages(packages)
158    ctx.run("twine upload {opts} dist/*".format(opts=" ".join(opts)))
159
160    # ctx.run("twine upload --repository={repo} dist/*".format(repo=repo))
161    # 2018-05-05 WORK-AROUND for new https://pypi.org/:
162    #   twine upload --repository-url=https://upload.pypi.org/legacy /dist/*
163    # NOT-WORKING: repo_url = "https://upload.pypi.org/simple/"
164    #
165    # ctx.run("twine upload --repository-url={repo_url} {opts} dist/*".format(
166    #    repo_url=repo_url, opts=" ".join(opts)))
167    # ctx.run("twine upload --repository={repo} {opts} dist/*".format(
168    #         repo=repo, opts=" ".join(opts)))
169
170
171# -- DEPRECATED: Use RTD instead
172# @task(name="upload_docs")
173# def upload_docs(ctx, repo=None, dry_run=False):
174#     """Upload and publish docs.
175#
176#     NOTE: Docs are built first.
177#     """
178#     if repo is None:
179#         repo = ctx.project.repo or "pypi"
180#     if dry_run:
181#         ctx = DryRunContext(ctx)
182#
183#     ctx.run("python setup.py upload_docs")
184#
185# -----------------------------------------------------------------------------
186# TASK HELPERS:
187# -----------------------------------------------------------------------------
188def print_packages(packages):
189    print("PACKAGES[%d]:" % len(packages))
190    for package in packages:
191        package_size = package.stat().st_size
192        package_time = package.stat().st_mtime
193        print("  - %s  (size=%s)" % (package, package_size))
194
195
196def ensure_packages_exist(ctx, pattern=None, check_only=False):
197    if pattern is None:
198        project_name = ctx.project.name
199        project_prefix = project_name.replace("_", "-").split("-")[0]
200        pattern = "dist/%s*" % project_prefix
201
202    packages = list(path_glob(pattern, current_dir="."))
203    if not packages:
204        if check_only:
205            message = "No artifacts found: pattern=%s" % pattern
206            raise RuntimeError(message)
207        else:
208            # -- RECURSIVE-SELF-CALL: Once
209            print("NO-PACKAGES-FOUND: Build packages first ...")
210            build_packages(ctx, hide=True)
211            packages = ensure_packages_exist(ctx, pattern,
212                                             check_only=True)
213    return packages
214
215
216# -----------------------------------------------------------------------------
217# TASK CONFIGURATION:
218# -----------------------------------------------------------------------------
219# DISABLED: register_packages
220namespace = Collection(bump_version, checklist, prepare, build_packages, upload)
221namespace.configure({
222    "project": {
223        "repo": "pypi",
224        "repo_url": None,
225    }
226})
227