summaryrefslogtreecommitdiffhomepage
path: root/tasks.py
blob: 2e1052977f2a8810abbc900639ced54cf690e0fd (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
import os
from os.path import join
from shutil import rmtree, copytree

from invoke import Collection, task
from invocations.checks import blacken
from invocations.docs import docs, www, sites
from invocations.packaging.release import ns as release_coll, publish
from invocations.testing import count_errors


# TODO: this screams out for the invoke missing-feature of "I just wrap task X,
# assume its signature by default" (even if that is just **kwargs support)
@task
def test(
    ctx,
    verbose=True,
    color=True,
    capture="sys",
    module=None,
    k=None,
    x=False,
    opts="",
    coverage=False,
    include_slow=False,
    loop_on_fail=False,
):
    """
    Run unit tests via pytest.

    By default, known-slow parts of the suite are SKIPPED unless
    ``--include-slow`` is given. (Note that ``--include-slow`` does not mesh
    well with explicit ``--opts="-m=xxx"`` - if ``-m`` is found in ``--opts``,
    ``--include-slow`` will be ignored!)
    """
    if verbose and "--verbose" not in opts and "-v" not in opts:
        opts += " --verbose"
    # TODO: forget why invocations.pytest added this; is it to force color when
    # running headless? Probably?
    if color:
        opts += " --color=yes"
    opts += " --capture={}".format(capture)
    if "-m" not in opts and not include_slow:
        opts += " -m 'not slow'"
    if k is not None and not ("-k" in opts if opts else False):
        opts += " -k {}".format(k)
    if x and not ("-x" in opts if opts else False):
        opts += " -x"
    if loop_on_fail and not ("-f" in opts if opts else False):
        opts += " -f"
    modstr = ""
    if module is not None:
        # NOTE: implicit test_ prefix as we're not on pytest-relaxed yet
        modstr = " tests/test_{}.py".format(module)
    # Switch runner depending on coverage or no coverage.
    # TODO: get pytest's coverage plugin working, IIRC it has issues?
    runner = "pytest"
    if coverage:
        # Leverage how pytest can be run as 'python -m pytest', and then how
        # coverage can be told to run things in that manner instead of
        # expecting a literal .py file.
        runner = "coverage run -m pytest"
    # Strip SSH_AUTH_SOCK from parent env to avoid pollution by interactive
    # users.
    # TODO: once pytest coverage plugin works, see if there's a pytest-native
    # way to handle the env stuff too, then we can remove these tasks entirely
    # in favor of just "run pytest"?
    env = dict(os.environ)
    if "SSH_AUTH_SOCK" in env:
        del env["SSH_AUTH_SOCK"]
    cmd = "{} {} {}".format(runner, opts, modstr)
    # NOTE: we have a pytest.ini and tend to use that over PYTEST_ADDOPTS.
    ctx.run(cmd, pty=True, env=env, replace_env=True)


@task
def coverage(ctx, opts="", codecov=False):
    """
    Execute all tests (normal and slow) with coverage enabled.
    """
    test(ctx, coverage=True, include_slow=True, opts=opts)
    # Cribbed from invocations.pytest.coverage for now
    if codecov:
        ctx.run("coverage xml")
        ctx.run("codecov")


@task
def guard(ctx, opts=""):
    """
    Execute all tests and then watch for changes, re-running.
    """
    # TODO if coverage was run via pytest-cov, we could add coverage here too
    return test(ctx, include_slow=True, loop_on_fail=True, opts=opts)


# Until we stop bundling docs w/ releases. Need to discover use cases first.
# TODO: would be nice to tie this into our own version of build() too, but
# still have publish() use that build()...really need to try out classes!
# TODO 3.0: I'd like to just axe the 'built docs in sdist', none of my other
# projects do it.
@task
def publish_(
    ctx, sdist=True, wheel=True, sign=True, dry_run=False, index=None
):
    """
    Wraps invocations.packaging.publish to add baked-in docs folder.
    """
    # Build docs first. Use terribad workaround pending invoke #146
    ctx.run("inv docs", pty=True, hide=False)
    # Move the built docs into where Epydocs used to live
    target = "docs"
    rmtree(target, ignore_errors=True)
    # TODO: make it easier to yank out this config val from the docs coll
    copytree("sites/docs/_build", target)
    # Publish
    publish(
        ctx, sdist=sdist, wheel=wheel, sign=sign, dry_run=dry_run, index=index
    )


# Also have to hack up the newly enhanced all_() so it uses our publish
@task(name="all", default=True)
def all_(c, dry_run=False):
    release_coll["prepare"](c, dry_run=dry_run)
    publish_(c, dry_run=dry_run)
    release_coll["push"](c, dry_run=dry_run)
    release_coll["tidelift"](c, dry_run=dry_run)


# TODO: "replace one task with another" needs a better public API, this is
# using unpublished internals & skips all the stuff add_task() does re:
# aliasing, defaults etc.
release_coll.tasks["publish"] = publish_
release_coll.tasks["all"] = all_

ns = Collection(
    test,
    coverage,
    guard,
    release_coll,
    docs,
    www,
    sites,
    count_errors,
    blacken,
)
ns.configure(
    {
        "packaging": {
            # NOTE: many of these are also set in kwarg defaults above; but
            # having them here too means once we get rid of our custom
            # release(), the behavior stays.
            "sign": True,
            "wheel": True,
            "changelog_file": join(
                www.configuration()["sphinx"]["source"], "changelog.rst"
            ),
        }
    }
)