Skip to content

DRY task arguments #153

Open
Open

Description

@pmdarrow

I've got several tasks that take the same few arguments with the same help text strings and same defaults. Is there a way to make this more DRY? E.g.

@task(help={
    'foo-arg': '...',
    'common-arg-1': 'Common arg 1 help text',
    'common-arg-2': 'Common arg 2 help text'
})
def foo_task(foo_arg='...', common_arg_1=True, common_arg_2=False):
    ...

@task(help={
    'bar-arg': '...',
    'common-arg-1': 'Common arg 1 help text',
    'common-arg-2': 'Common arg 2 help text'
})
def bar_task(bar_arg='...', common_arg_1=True, common_arg_2=False):
    ...

Activity

  1. changed the title [-]Common task arguments[/-] [+]DRY task arguments[/+] on Jun 19, 2014
  2. sophacles commented on Jun 19, 2014

    @sophacles

    Using contexts helps some, and will really do so once #147 is resolved

  3. pmdarrow commented on Jun 20, 2014

    @pmdarrow
    Author

    How would contexts help in this scenario?

  4. bitprophet commented on Aug 22, 2014

    @bitprophet
    Member

    @pmdarrow There's nothing built-in yet that helps much here, though I'd like to add it sometime. For the help itself you could just merge some dicts like so:

    common_arg_helps = {
        'common-arg-1': 'common arg 1 help text',
        'common-arg-2': 'common arg 2 help text',
    }
    
    # If 'foo-arg' were a valid literal, could just use keyword args here,
    # eg dict(xxx, foo_arg='...')
    @task(help=dict(common_arg_helps, **{'foo-arg': '...'})
    def foo_task(...):
        ...

    but you can't apply that trick to the actual function args (unless I forget something) because introspection tools like Invoke's parser (and Sphinx autodoc and so on) do not play well with def foo(**kwargs) - which is the only way I know of to merge function kwargs at runtime w/ a dict of default values.

    Again, this is something I'd like to improve; there's tickets open for related-ish issues though nothing quite like what I am thinking offhand, which is "more stuff in @task letting you override the default introspection at the parser level".


    @sophacles - Using contexts' configuration options wouldn't really help that much here besides possibly letting him offload the default kwarg values to a single location. (I.e. ye olde "default is None, then if None was given, look elsewhere for what the default should be" pattern.)

  5. sophacles commented on Aug 23, 2014

    @sophacles

    I think I misunderstood this ticket when I made my context argument. Agree that this is something to look at.

    I wonder if this has something to do with the idea of Collection - which is already grouping tasks based on commonality (currently whatever semantics one wants to attach to a namespace, and context variables).

    Just a random brainstorm here:

    from invoke import Collection, TaskArg
    
    typeA_tasks = Collection(common_args = {"foo": TaskArg(expected_type=int, help="about foo", default = 10),
                                                                        "bar": TaskArg(expeceted_type=str, help="about bar", default = "bar_default")},
                                            name = "typeA")
    @typeA_tasks.ctask(...)
    def task1(ctx, t1only="default1"):
        print "task1 {} {} {}".format(t1only, ctx.foo, ctx.bar)
    
    @typeA_tasks.ctask(...)
    def task2(ctx, t2only="default2"):
        print "task2 {} {} {}".format(t2only, ctx.foo, ctx.bar)
    

    So the Collection holds common args, and makes them avilable to the task via the context mechanism (as other collection things are passed). TheTaskArg class above could be a real thing in the future or not, but I put it above to a provide some semantic understanding in an imaginary example :)

    The other thing, is the option for adding tasks to a collection right away makes a lot of sense anyway, so the ctask() function above is really a merging of tasks.ctask() and Collection.add_task.

    So the expected output could be something like this:

    $ inv typeA.task1 --t1only example --bar example_bar
    task1 example 10 example_bar
    $ inv typeA.task1 --t1only example --bar example_bar typeA.task2 --t2only t2 --foo 22
    task1 example 10 example_bar
    task2 t2 22 bar_default
    

    And the --help generated for the typeA items would include --foo and --bar as arguments.

    A problem here, is that it provides another axis to collection separation, leading to deeper trees. A solution here, is to perhaps allow a Collection to subsume another Collection by "hiding" that there is a sub-collection from the command line. The collection is there in reality, however it doesn't show up in the namespace collection tree. Or have Namespace be a first class concept orthogonal to Collection.

    Anyway, just an idea - feel free to discard if it is not the right direction.

  6. bitprophet commented on Aug 25, 2014

    @bitprophet
    Member

    Yea, I dunno if that's the best approach. I actually made a new ticket this morning prompted by various things, #174, which I suspect will just turn into "let people slap explicit Argument objects into their @tasks".

    In that scenario, this issue here becomes partially solved, only needing us to figure out the interplay between "wholly implicit based on function args" (current setup), "wholly explicit based on an argument to @task" (described in #174) and "some mixture of the two" (this ticket's use case).

    I'm leaving this open for now (vs rolling into #174 right away) until I figure that part out.

  7. JamesRamm commented on Jan 7, 2019

    @JamesRamm

    This would be a cool feature - any progress on support?

    One workaround can be to subclass Task and insert the common arguments in get_arguments

    class TaskWithCommonArgument(Task):
      def get_arguments(self):
        args = super(TaskWithCommonArgument, self).get_arguments()
        args.append('host')
        return args

    Also add a new decorator to avoid specifying 'klass' all the time:

    def mytask(*args, **kwargs):
      kwargs['klass'] = TaskWithCommonArgument
      return task(*args, **kwargs)

    Then you can do something like:

    @mytask()
    def do_something(c, **kwargs):
       print(kwargs)
  8. TaiSHiNet commented on Dec 9, 2021

    @TaiSHiNet

    One issue with that approach @JamesRamm is that it doesn't support short flags (or defaults).
    Tried several approaches around this but none worked :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions