Skip to content

Commit ca2fd9b

Browse files
committed
Merge branch 'master' into 197-int
2 parents add7085 + 279b2e4 commit ca2fd9b

File tree

226 files changed

+33345
-7214
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

226 files changed

+33345
-7214
lines changed

.coveragerc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[run]
2+
branch = True
3+
include = invoke/*
4+
omit = invoke/vendor/*

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ dist/
66
*.egg-info
77
*.py[cod]
88
src/
9+
htmlcov
10+
coverage.xml
11+
.cache

.travis.yml

+55-16
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,79 @@
11
language: python
2+
sudo: required
3+
dist: xenial
4+
cache:
5+
directories:
6+
- $HOME/.cache/pip
27
python:
3-
- "2.6"
48
- "2.7"
5-
- "3.2"
6-
- "3.3"
79
- "3.4"
10+
- "3.5"
11+
- "3.6"
12+
- "3.7"
13+
- "3.8-dev"
814
- "pypy"
15+
- "pypy3"
916
matrix:
10-
# pypy is frequentlyish unstable on Travis, don't let it mark entire status
11-
# as 'bad' =/ (Still worth running it though.)
1217
allow_failures:
13-
- python: pypy
18+
- python: "3.8-dev"
19+
# WHY does this have to be in before_install and not install? o_O
20+
before_install:
21+
# Used by 'inv regression' (more performant/safe/likely to expose real issues
22+
# than in-Python threads...)
23+
- sudo apt-get -y install parallel
1424
install:
25+
# For some reason Travis' build envs have wildly different pip/setuptools
26+
# versions between minor Python versions, and this can cause many hilarious
27+
# corner packaging cases. So...
28+
- pip install -U pip
29+
# Setuptools 34+ seems to get less stable
30+
- pip install 'setuptools>33,<34'
1531
# Pre-requirements sanity test (again, resembles pure, non-dev install
1632
# environment.) Avoids e.g. spec's 'six' from gumming up our attempts to
1733
# import our vendorized 'six'.
1834
- pip install -r tasks-requirements.txt
1935
- inv --list
2036
# Install remaining dev requirements (test runner, etc)
2137
- pip install -r dev-requirements.txt
38+
- pip list --format=columns
39+
# Also create a workable alt-interpreter venv for testing dual package builds
40+
# Python 3 is nicely namespaced, globally. Python 2 is masked by Travis'
41+
# default venv, so we gotta hardcode it.
42+
- "virtualenv alt_env --python=$([[ $TRAVIS_PYTHON_VERSION == 2* ]] && echo python3 || echo /usr/bin/python)"
43+
- alt_env/bin/pip install wheel
44+
before_script:
45+
# Create 'sudouser' w/ sudo password & perms on Travis' homedir
46+
- inv travis.make-sudouser
47+
# Blacken and flake8 before running any tests, it's a faster fail
48+
- inv travis.blacken
49+
- flake8
2250
script:
23-
# Main test suite
24-
- inv test
25-
# Slow, annoying-to-run-every-time integration suite
26-
- inv integration
27-
# Websites build OK? (NOTE: sphinx/jinja are fucked on Python 3.2 now?)
28-
- "[[ $TRAVIS_PYTHON_VERSION == 3.2 ]] || inv sites"
51+
# Execute full test suite + coverage, as the new sudo-capable user
52+
- inv travis.sudo-coverage
53+
# Perform extra "not feasible inside pytest for no obvious reason" tests
54+
- inv regression
55+
# Websites build OK? (Not on PyPy3, Sphinx is all "who the hell are you?" =/
56+
- "if [[ $TRAVIS_PYTHON_VERSION != 'pypy3' ]]; then inv sites; fi"
57+
# Doctests in websites OK? (Same caveat as above...)
58+
- "if [[ $TRAVIS_PYTHON_VERSION != 'pypy3' ]]; then inv www.doctest; fi"
2959
# Did we break setup.py?
30-
- pip uninstall -y invoke # To undo the implicit -e install from above
31-
- pip install . # NO -e!
32-
- inv -l # Sanity check
60+
# NOTE: sometime in 2019 travis grew a bizarre EnvironmentError problem
61+
# around inability to overwrite/remote __pycache__ dirs...this attempts to
62+
# workaround
63+
- "find . -type d -name __pycache__ | sudo xargs rm -rf"
64+
- inv travis.test-installation --package=invoke --sanity="inv --list"
65+
# Test distribution builds, including some package_data based stuff
66+
# (completion script printing)
67+
- "inv travis.test-packaging --package=invoke --sanity='inv --list && inv --print-completion-script zsh' --alt-python=alt_env/bin/python"
68+
after_success:
69+
# Upload coverage data to codecov
70+
- codecov
3371
notifications:
3472
irc:
3573
channels: "irc.freenode.org#invoke"
3674
template:
37-
- "%{repository}@%{branch}: %{message} (%{build_url})"
75+
- "%{repository_name}@%{branch}: %{message} (%{build_url})"
3876
on_success: change
3977
on_failure: change
78+
on_error: change
4079
email: false

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2014 Jeff Forcier.
1+
Copyright (c) 2020 Jeff Forcier.
22
All rights reserved.
33

44
Redistribution and use in source and binary forms, with or without

MANIFEST.in

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
include LICENSE
22
include README.rst
3-
recursive-include docs *
4-
recursive-exclude docs/_build *
3+
include tasks.py
4+
recursive-include invoke/completion *
5+
recursive-include sites *
6+
recursive-exclude sites/*/_build *
57
include dev-requirements.txt
8+
include tasks-requirements.txt
69
recursive-include tests *
7-
recursive-exclude tests *.pyc *.pyo
10+
recursive-exclude * *.pyc *.pyo
11+
recursive-exclude **/__pycache__ *

README.rst

+9-47
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,11 @@
1-
Invoke is a Python (2.6+ and 3.2+) task execution tool & library, drawing inspiration from various sources to arrive at a powerful & clean feature set.
1+
Welcome to Invoke!
2+
==================
23

3-
* Like Ruby's Rake tool and Invoke's own predecessor Fabric 1.x, it provides a
4-
clean, high level API for running shell commands and defining/organizing
5-
task functions from a ``tasks.py`` file::
4+
Invoke is a Python (2.7 and 3.4+) library for managing shell-oriented
5+
subprocesses and organizing executable Python code into CLI-invokable tasks. It
6+
draws inspiration from various sources (``make``/``rake``, Fabric 1.x, etc) to
7+
arrive at a powerful & clean feature set.
68

7-
from invoke import run, task
8-
9-
@task
10-
def clean(docs=False, bytecode=False, extra=''):
11-
patterns = ['build']
12-
if docs:
13-
patterns.append('docs/_build')
14-
if bytecode:
15-
patterns.append('**/*.pyc')
16-
if extra:
17-
patterns.append(extra)
18-
for pattern in patterns:
19-
run("rm -rf %s" % pattern)
20-
21-
@task
22-
def build(docs=False):
23-
run("python setup.py build")
24-
if docs:
25-
run("sphinx-build docs docs/_build")
26-
27-
* From GNU Make, it inherits an emphasis on minimal boilerplate for common
28-
patterns and the ability to run multiple tasks in a single invocation::
29-
30-
$ invoke clean build
31-
32-
* Following the lead of most Unix CLI applications, it offers a traditional
33-
flag-based style of command-line parsing, deriving flag names and value types
34-
from task signatures (optionally, of course!)::
35-
36-
$ invoke clean --docs --bytecode build --docs --extra='**/*.pyo'
37-
$ invoke clean -d -b build --docs -e '**/*.pyo'
38-
$ invoke clean -db build -de '**/*.pyo'
39-
40-
* Like many of its predecessors, it offers advanced features as well --
41-
namespacing, task aliasing, before/after hooks, parallel execution and more.
42-
43-
For documentation, including detailed installation information, please see
44-
http://docs.pyinvoke.org. Post-install usage information may be found in ``invoke
45-
--help``.
46-
47-
You can install the `development version
48-
<https://github.com/pyinvoke/invoke/tarball/master#egg=invoke-dev>`_ via ``pip
49-
install invoke==dev``.
9+
For a high level introduction, including example code, please see `our main
10+
project website <http://pyinvoke.org>`_; or for detailed API docs, see `the
11+
versioned API website <http://docs.pyinvoke.org>`_.

THOUGHTS.rst

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
==============================================
2+
Random thoughts unsuitable for public docs yet
3+
==============================================
4+
5+
CLI type mapping
6+
================
7+
8+
Some loose thoughts on bridging the "shell is strings, Python wants
9+
lists/dicts/integers/bools/etc" problem.
10+
11+
Methodologies
12+
-------------
13+
14+
* Explicit mapping, as with ``argparse``: this particular flag turns into a
15+
list/boolean/int/whatever. Because we're specifically mapping to function
16+
keyword arguments, a little of that complexity can be removed, but generally
17+
it'll look very similar. E.g.::
18+
19+
@args(foo=int)
20+
def mytask(foo):
21+
...
22+
23+
would turn this::
24+
25+
$ invoke mytask --foo 7
26+
27+
into ``7``, not ``"7"``.
28+
* Introspection-based mapping, i.e. introspecting the default values of a
29+
function signature and automatically transforming the CLI input. E.g.::
30+
31+
def mytask(foo=5):
32+
...
33+
34+
invoked as::
35+
36+
$ invoke mytask --foo 7
37+
38+
results in the Python value ``7`` instead of ``"7"``, just as with the
39+
explicit example above.
40+
* Formatting-based mapping, i.e. having (optional) conventions in the string
41+
format of an incoming flag argument that cause transformations to occur.
42+
E.g. we could say that commas in an argument automatically trigger
43+
transformation into a list of strings; thus the invocation::
44+
45+
$ invoke mytask --items a,b,c
46+
47+
would on the Python end turn into a call like this::
48+
49+
mytask(items=['a', 'b', 'c'])
50+
51+
What to do?
52+
~~~~~~~~~~~
53+
54+
We haven't decided exactly how many of these to use -- we may end up using all
55+
three of them as appropriate, with some useful/sensible default and the option
56+
to enable/disable things for power users. The trick is to balance
57+
power/features with becoming overly complicated to understand or utilize.
58+
59+
Other types
60+
-----------
61+
62+
Those examples cover integers/numbers, and lists/iterables. Strings are
63+
obviously easy/the default. What else is there?
64+
65+
* Booleans: these are relatively simple too, either a flag exists (``True``) or
66+
is omitted (``False``).
67+
68+
* Could also work in a ``--foo`` vs ``--no-foo`` convention to help with
69+
the inverse, i.e. values which should default to ``True`` and then need
70+
to be turned "off" on the command line. E.g.::
71+
72+
def mytask(option=True):
73+
...
74+
75+
could result in having a flag called ``--no-option`` instead of
76+
``--option``. (Or possibly both.)
77+
78+
* Dicts: these are tougher, but we could potentially use something like::
79+
80+
$ invoke mytask --dictopt key1=val1,key2=val2
81+
82+
resulting in::
83+
84+
mytask(dictopt={'key1': 'val1', 'key2': 'val2'})
85+
86+
87+
Parameterizing tasks
88+
====================
89+
90+
Old "previous example" (at time the below was split out of live docs, the
91+
actual previous example had been changed a lot and no longer applied)::
92+
93+
$ invoke test --module=foo test --module=bar
94+
Cleaning
95+
Testing foo
96+
Cleaning
97+
Testing bar
98+
99+
The previous example had a bit of duplication in how it was invoked; an
100+
intermediate use case is to bundle up that sort of parameterization into a
101+
"meta" task that itself invokes other tasks in a parameterized fashion.
102+
103+
TK: API for this? at CLI level would have to be unorthodox invocation, e.g.::
104+
105+
@task
106+
def foo(bar):
107+
print(bar)
108+
109+
$ invoke --parameterize foo --param bar --values 1 2 3 4
110+
1
111+
2
112+
3
113+
4
114+
115+
Note how there's no "real" invocation of ``foo`` in the normal sense. How to
116+
handle partial application (e.g. runtime selection of other non-parameterized
117+
arguments)? E.g.::
118+
119+
@task
120+
def foo(bar, biz):
121+
print("%s %s" % (bar, biz))
122+
123+
$ invoke --parameterize foo --param bar --values 1 2 3 4 --biz "And a"
124+
And a 1
125+
And a 2
126+
And a 3
127+
And a 4
128+
129+
That's pretty clunky and foregoes any multi-task invocation. But how could we
130+
handle multiple tasks here? If we gave each individual task flags for this,
131+
like so::
132+
133+
$ invoke foo --biz "And a" --param foo --values 1 2 3 4
134+
135+
We could do multiple tasks, but then we're stomping on tasks' argument
136+
namespaces (we've taken over ``param`` and ``values``). Really hate that.
137+
138+
**IDEALLY** we'd still limit parameterization to library use since it's an
139+
advanced-ish feature and frequently the parameterization vector is dynamic (aka
140+
not the sort of thing you'd give at CLI anyway)
141+
142+
Probably best to leave that in the intermediate docs and keep it lib level;
143+
it's mostly there for Fabric and advanced users, not something the average
144+
Invoke-only user would care about. Not worth the effort to make it work on CLI
145+
at this point.
146+
147+
::
148+
149+
@task
150+
def stuff(var):
151+
print(var)
152+
153+
# NOTE: may need to be part of base executor since Collection has to know
154+
# to pass the parameterization option/values into Executor().execute()?
155+
class ParameterizedExecutor(Executor):
156+
# NOTE: assumes single dimension of parameterization.
157+
# Realistically would want e.g. {'name': [values], ...} structure and
158+
# then do cross product or something
159+
def execute(self, task, args, kwargs, parameter=None, values=None):
160+
# Would be nice to generalize this?
161+
if parameter:
162+
# TODO: handle non-None parameter w/ None values (error)
163+
# NOTE: this is where parallelization would occur; probably
164+
# need to move into sub-method
165+
for value in values:
166+
my_kwargs = dict(kwargs)
167+
my_kwargs[parameter] = value
168+
super(self, ParameterizedExecutor).execute(task, kwargs=my_kwargs)
169+
else:
170+
super(self, ParameterizedExecutor).execute(task, args, kwargs)
171+
172+
173+
Getting hairy: one task, with one pre-task, parameterized
174+
=========================================================
175+
176+
::
177+
178+
@task
179+
def setup():
180+
print("Yay")
181+
182+
@task(pre=[setup])
183+
def build():
184+
print("Woo")
185+
186+
class OhGodExecutor(Executor):
187+
def execute(self, task, args, kwargs, parameter, values):
188+
# assume always parameterized meh
189+
# Run pretasks once only, instead of once per parameter value
190+
for pre in task.pre:
191+
self.execute(self.collection[pre])
192+
for value in values:
193+
my_kwargs = dict(kwargs)
194+
my_kwargs[parameter] = value
195+
super(self, OhGodExecutor).execute(task, kwargs=my_kwargs)
196+
197+
198+
Still hairy: one task, with a pre-task that itself has a pre-task
199+
=================================================================
200+
201+
All the things: two tasks, each with pre-tasks, both parameterized
202+
==================================================================

0 commit comments

Comments
 (0)