2

I'm trying to pass contexts between two subcommands with python-click. Here's an MWE:

import click


@click.group(chain=True)
def cli() -> None:
    pass


@cli.command()
@click.pass_context
def fn1(cxt):
    cxt.obj = 1


@cli.command()
@click.pass_context
def fn2(cxt):
    print(f'{cxt.obj=}')


if __name__ == '__main__':
    cli()

If I call this with cli.py fn1 fn2, I expect to get "cxt.obj=1", whereas I get "cxt.obj=None".

While trying to debug, I also noticed I can access the cli's context from fn1 and fn2, but not fn1's context from fn2. I can therefore, set the object of cli's context in fn1, and read from cli's context in fn2, but there must be a better way, where the obj is directly accessible in fn2's context.

What is wrong with my mental model, why doesn't the context persist between subcommands? And, what is the best pattern to use when one needs to pass data between subcommands?

1 Answer 1

2

The context is copied from a parent to a child. In your case context.obj for cli() which you do not expose in your example, is None. The None is copied into the child context for fn1() and fn2(), but then in fn1() you change the copy to 1. Finally, in fn2() you receive a copy of the context for cli() in which obj is still None

So to achieve your desired result there are (at least) two options.

  1. Make context.obj on the parent context a mutable object, and then mutate said object
  2. As you indicated was possible, directly access the parent context via something like ctx.parent.obj = 1

An example of the first (my preferred) method:

Example

import click, sys


@click.group(chain=True)
@click.pass_context
def cli(ctx) -> None:
    ctx.obj = {}


@cli.command()
@click.pass_context
def fn1(ctx):
    ctx.obj['fn1'] = 1


@cli.command()
@click.pass_context
def fn2(ctx):
    click.echo(f'obj: {ctx.obj}')

Test Code

if __name__ == "__main__":
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    print('-----------')
    cmd = 'fn1 fn2'
    print('> ' + cmd)
    cli(cmd.split())

Results:

Click Version: 8.1.3
Python Version: 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)]
-----------
> fn1 fn2
obj: {'fn1': 1}
Sign up to request clarification or add additional context in comments.

1 Comment

Great example, but the test here is not testing the right behaviour. The test chains the commands in the same execution, which actually doesn't showcase the main behaviour in a cli. In a cli you would manually run fn1 and then manually run fn2. Passing context here is taken as passing context within one execution, but really you need to pass context across executions. eg. git status (return no files added and some changed files), git add . (adds a bunch of changed files), git status (now shows the update list of files). The example fails when using the above example fn1, fn2

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.