|
| 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