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}
Answer from Stephen Rauch on Stack Overflow
🌐
AssemblyAI
assemblyai.com › blog › the-definitive-guide-to-python-click
The Definitive Guide to Python Click
July 28, 2021 - All we have to do to make this command a part of the group we created earlier is decorate our function with the decorator <group name>.command(<command name>). To make sure that this object is able to access context, we also use the pass_context decorator. Now that we have some familiarity with decorators and Python Click, let’s use Click’s functionality to make our own pass decorator.
Discussions

python - Click: Use another function in chained commands with context object - Stack Overflow
I recently have been using the click package to build command line interfaces which has worked perfectly, so far. Now I got into some trouble when using chained commands in combination with the co... More on stackoverflow.com
🌐 stackoverflow.com
python - Multiple context objects in CLI (Click) - Stack Overflow
1 Command line interface with multiple commands using click: add unspecified options for command as dictionary · 4 With python click, how can I avoid duplicating argument code used by multiple subcommands More on stackoverflow.com
🌐 stackoverflow.com
python - How can I pass a ctx (Context) to CliRunner? - Stack Overflow
The key part of this answer is ... @click.pass_context decorator and ctx parameter for catching it. ... The first argument should be the the root group of click, then you can pass the command you want to call to the second argument(that is sayhello). ... # !/usr/bin/env python # coding: ... More on stackoverflow.com
🌐 stackoverflow.com
python - How to get access to click context in an option decorator? - Stack Overflow
I am trying to create a cli application using Python click and I want to use a function I assigned to click.core.Context.obj as the callback function of an option. The function I use as a callback More on stackoverflow.com
🌐 stackoverflow.com
🌐
Readthedocs
pocoo-click.readthedocs.io › en › latest › api
API - the Click Documentation
Commands are the basic building block of command line interfaces in Click. A basic command handles command line parsing and might dispatch more parsing to commands nested below it. Changed in version 2.0: Added the context_settings parameter.
🌐
ProgramCreek
programcreek.com › python › example › 88034 › click.Context
Python Examples of click.Context
:param click_context: click context object. :param agents: agents names. :param multithreaded: bool flag to run as multithreads. :return: None.
🌐
Digitalk
digitalk.tech › blog › python-click-or-how-to-write-professional-cli-applications
Mndwrk Blog - Python click or how to write professional CLI applications
June 17, 2022 - Finally, the ctx parameter is the currently active click execution context (click.Context object). Another interesting thing is the call to self.fail, which will raise a click-specific exception. This exception will then be used to display an error if the input is not a valid JSON file, e.g.: $ echo 'not-a-json' | python3 json_ops.py list-key -k key Usage: json_ops.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... Try 'json_ops.py --help' for help.
🌐
Click Documentation
click.palletsprojects.com › en › stable › complex
Complex Applications — Click Documentation (8.5.x)
Whenever a Click command is executed, a Context object is created which holds state for this particular invocation. It remembers parsed parameters, what command created it, which resources need to be cleaned up at the end of the function, and so forth.
Find elsewhere
🌐
dbader.org
dbader.org › blog › mastering-click-advanced-python-command-line-apps
Mastering Click: Writing Advanced Python Command-Line Apps – dbader.org
January 2, 2018 - What we are doing here is telling Click that we want access to the context of the command (or group) and Click will pass it to our function as the first argument, I called it ctx. In the function itself, we can now set the obj attribute on the context to an empty dictionary that we can then fill with data.
Top answer
1 of 1
7

If you can edit your click command functions, you can organize them like this:

@cli.command()
@click.argument('some_argument', type=str)
@click.pass_context
def say_something(ctx, some_argument):
    return _say_something(ctx, some_argument):

def _say_something(ctx, some_argument):
    print(some_argument)

If built like this, then you can call _say_something() function as an undecorated (normal) Python function.

If you can not edit your commands

Building on this answer you can pass the context to another click command using this function:

Code:

def call_click_command_with_ctx(cmd, ctx, *args, **kwargs):
    """ Wrapper to call a click command with a Context object

    :param cmd: click cli command function to call
    :param ctx: click context
    :param args: arguments to pass to the function
    :param kwargs: keyword arguments to pass to the function
    :return: None
    """

    # monkey patch make_context
    def make_context(*some_args, **some_kwargs):
        child_ctx = click.Context(cmd, parent=ctx)
        with child_ctx.scope(cleanup=False):
            cmd.parse_args(child_ctx, list(args))
        return child_ctx

    cmd.make_context = make_context
    prev_make_context = cmd.make_context

    # call the command
    call_click_command(cmd, *args, **kwargs)

    # restore make_context
    cmd.make_context = prev_make_context

How does this work?

This works because click is a well designed OO framework. The @click.Command object can be introspected to determine what parameters it is expecting. Then a command line can be constructed that will look like the command line that click is expecting. In addition, the make_context method of the command can be overridden to allow the command context to used by the command.

Code from Previous Answer:

def call_click_command(cmd, *args, **kwargs):
    """ Wrapper to call a click command

    :param cmd: click cli command function to call
    :param args: arguments to pass to the function
    :param kwargs: keywrod arguments to pass to the function
    :return: None
    """

    # Get positional arguments from args
    arg_values = {c.name: a for a, c in zip(args, cmd.params)}
    args_needed = {c.name: c for c in cmd.params
                   if c.name not in arg_values}

    # build and check opts list from kwargs
    opts = {a.name: a for a in cmd.params if isinstance(a, click.Option)}
    for name in kwargs:
        if name in opts:
            arg_values[name] = kwargs[name]
        else:
            if name in args_needed:
                arg_values[name] = kwargs[name]
                del args_needed[name]
            else:
                raise click.BadParameter(
                    "Unknown keyword argument '{}'".format(name))


    # check positional arguments list
    for arg in (a for a in cmd.params if isinstance(a, click.Argument)):
        if arg.name not in arg_values:
            raise click.BadParameter("Missing required positional"
                                     "parameter '{}'".format(arg.name))

    # build parameter lists
    opts_list = sum(
        [[o.opts[0], str(arg_values[n])] for n, o in opts.items()], [])
    args_list = [str(v) for n, v in arg_values.items() if n not in opts]

    # call the command
    cmd(opts_list + args_list)

Test Code:

import click

@click.group(chain=True)
@click.option('--some_common_option', type=float, default=1e-10)
@click.pass_context
def cli(ctx, some_common_option):
    # save shared params within context object for different commands
    for k, v in locals().items():
        if 'ctx' not in k:
            ctx.obj[k] = v


@cli.command()
@click.argument('some_argument', type=str)
@click.pass_context
def say_something(ctx, some_argument):
    print(some_argument)


@cli.command()
@click.argument('some_other_argument', type=str)
@click.pass_context
def say_more(ctx, some_other_argument):
    ctx.obj['text'] = some_other_argument
    call_click_command_with_ctx(say_something, ctx, ctx.obj['text'])


if __name__ == "__main__":
    commands = (
        'say_something something',
        'say_more more',
        '--help',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            cli(cmd.split(), obj={})

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Results:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> say_something something
something
-----------
> say_more more
more
-----------
> --help
Usage: test.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...

Options:
  --some_common_option FLOAT
  --help                      Show this message and exit.

Commands:
  say_more
  say_something
🌐
Click Documentation
click.palletsprojects.com › en › stable › commands
Advanced Groups and Context — Click Documentation (8.5.x)
For instance, a common pattern would be to configure an object representing state and then storing it on the context and then to use a custom decorator to find the most recent object of this sort and pass it as first argument. For instance, the pass_obj() decorator can be implemented like this: from functools import update_wrapper def pass_obj(f): @click.pass_context def new_func(ctx, *args, **kwargs): return ctx.invoke(f, ctx.obj, *args, **kwargs) return update_wrapper(new_func, f)
🌐
Click Documentation
click.palletsprojects.com › en › stable › advanced
Advanced Patterns — Click Documentation (8.5.x)
It can be useful to open a resource in a group, to be made available to subcommands. Many types of resources need to be closed or otherwise cleaned up after use. The standard way to do this in Python is by using a context manager with the with statement.
Top answer
1 of 3
9

You would directly pass your Config instance as keyword argument obj to runner.invoke:

import click
from click.testing import CliRunner

class Config():
    def __init__(self):
        self.value = 651

@click.command()
@click.pass_obj
def print_numberinfo(obj):
    if not hasattr(obj, 'value'):
        obj = Config()
    click.echo(obj.value)

def test_print_numberinfo():
    obj = Config()
    obj.value = 777
    runner = CliRunner()
    # how do I pass ctx to runner.invoke?
    result = runner.invoke(print_numberinfo, obj=obj)
    assert result.output == str(obj.value) + '\n'
2 of 3
1

I had the same problem. I wanted to pass a db session from the test side to my command method.

Here's my related code snippets and workaround using @click.pass_context:

command method

import asyncclick as click
import inspect
from getpass import getpass
from . import crud, schemas, database

class Provide:
    def __init__(self, value):
        self.value = value

def inject_db(f):
    sig = inspect.signature(f)
    async def wrapper(*args, **kwargs):
        for param in sig.parameters.values():
            if isinstance(param.default, Provide):
                async for db in param.default.value():
                    kwargs[param.name] = db
                    await f(*args, **kwargs)
    return wrapper

@click.group()
@click.pass_context
async def cli(ctx):
    pass

@cli.command(name="create_superuser")
@click.pass_context
@inject_db
async def create_superuser(ctx, db: database.AsyncSession = Provide(database.get_db)):
    if ctx.obj is not None:
        """ctx comes from test"""
        db = ctx.obj

    username = click.prompt("Username", type=str)
    email = click.prompt("Email (optional)", type=str, default="")
    password = getpass("Password: ")
    confirm_password = getpass("Confirm Password: ")

    if password != confirm_password:
        click.echo("Passwords do not match")
        return

    user = schemas.UserAdminCreate(
        username=username,
        email=None if not email else email,
        password=password,
        role="admin",
    )
    await crud.create_user(db=db, user=user)

if __name__ == "__main__":
    cli()

cli.add_command(create_superuser)

test part

@pytest.mark.asyncio
@patch("ai3d.commands.click.prompt", side_effect=["testuser", "[email protected]"])
@patch("ai3d.commands.getpass", side_effect=["testpassword", "testpassword"])
async def test_create_superuser(
    mock_getpass, mock_prompt, mock_get_hashed_password, db: AsyncSession
):
    with mock_get_hashed_password:
        runner = CliRunner()
        result = await runner.invoke(commands.create_superuser, obj=db)

        assert result.exit_code == 0
        mock_prompt.assert_any_call("Username", type=str)
        mock_prompt.assert_any_call("Email (optional)", type=str, default="")
        mock_getpass.assert_any_call("Password: ")
        mock_getpass.assert_any_call("Confirm Password: ")

        user = await crud.get_user(db=db, username="testuser")
        assert user.username == "testuser"
        assert user.email == "[email protected]"
        assert user.password == "hashedtestpassword"
        assert user.role == "admin"

[NOTE]:

  • I should add that in my case I'm using the asynclick package which is a fork of click providing async stuff, that's why I used async-awiat within the code.
  • The key part of this answer is passing the object through the obj= parameter in CliRunner().invoke() method and using @click.pass_context decorator and ctx parameter for catching it.
🌐
Readthedocs
pocoo-click.readthedocs.io › en › latest › advanced
Advanced Patterns - the Click Documentation
Starting with Click 5.0 it is possible to access the current context from anywhere within the same thread through the use of the get_current_context() function which returns it. This is primarily useful for accessing the context bound object as well as some flags that are stored on it to customize ...
Top answer
1 of 1
2

I would suggest adding a @staticmethod to your Config class to use as the target of the callback like:

Callback Method:

@staticmethod
def click_callback(ctx, param, value):
    getattr(ctx.obj, ctx.obj.func_name)()
    return value

Test Code:

import click

class Config:
    def __init__(self, func_name):
        self.func_name = func_name

    def func_a(self):
        click.echo('Func A')

    def func_b(self):
        click.echo('Func B')

    @staticmethod
    def click_callback(ctx, param, value):
        getattr(ctx.obj, ctx.obj.func_name)()
        return value


@click.group()
@click.option('-f', '--func-name', required=True)
@click.pass_context
def group(ctx, func_name):
    ctx.obj = Config(func_name)


@group.command()
@click.option('--my-option', '-o', callback=Config.click_callback)
def command(my_option):
    click.echo('command: {}'.format(my_option))


if __name__ == "__main__":
    commands = (
        '-f func_a command -o optionA',
        '-f func_b command -o optionB',
        '-f func_a command -o ',
        'command',
        '--help',
        '',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            group(cmd.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Results:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> -f func_a command -o optionA
Func A
command: optionA
-----------
> -f func_b command -o optionB
Func B
command: optionB
-----------
> -f func_a command -o 
Error: -o option requires an argument
-----------
> command
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Error: Missing option "-f" / "--func-name".
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  -f, --func-name TEXT  [required]
  --help                Show this message and exit.

Commands:
  command
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  -f, --func-name TEXT  [required]
  --help                Show this message and exit.

Commands:
  command
🌐
Click Documentation
click.palletsprojects.com › en › stable › commands-and-groups
Basic Commands, Groups, Context — Click Documentation (8.5.x)
Starting with Click 5.0 it is possible to access the current context from anywhere within the same thread through the use of the get_current_context() function which returns it. This is primarily useful for accessing the context bound object as well as some flags that are stored on it to customize ...
🌐
iO Flood
ioflood.com › blog › python-click-library-guide-easily-build-command-line-applications-in-python
Python Click Library Guide: Easily Build Command Line Applications in Python
February 4, 2024 - Next in line is understanding the concept of context in Python Click. Context is vital because it allows you to pass data between commands. This is implemented using the pass_context decorator.
🌐
Blue Book
lyz-code.github.io › blue-book › coding › python › click
Click - The Blue Book
Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It’s the “Command Line Interface Creation Kit”. It’s highly configurable but comes with sensible defaults out of the box · Click has the following features:
🌐
Paiml
paiml.com › docs › home › books › python-command-line-tools › chapter01-getting-started-click
Chapter01 Getting Started Click | Pragmatic AI Labs and Solutions
The new decorator starts with the name of the function of the group (main() in this case is @main.command()). Finally, the context is requested with @click.pass_context and declared as an argument (ctx). This context is what allows the function to print the help menu from the parent command (in main()), instead of generating some other help menu. Run this once again to see how it behaves with the new sub-command: $ python cli.py help Usage: cli.py [OPTIONS] COMMAND [ARGS]... Options: -h, --help Show this message and exit.