在上下文管理器中运行子命令

时间:2019-03-01 18:50:05

标签: python python-3.x command-line-interface python-click

在python click CLI应用程序的上下文中,我想在上下文管理器中运行一个子命令,该命令将在更高级别的命令中进行设置。如何使用click来做到这一点?我的伪代码看起来像:


import click

from contextlib import contextmanager

@contextmanager
def database_context(db_url):
    try:
        print(f'setup db connection: {db_url}')
        yield
    finally:
        print('teardown db connection')


@click.group
@click.option('--db',default='local')
def main(db):
    print(f'running command against {db} database')
    db_url = get_db_url(db)
    connection_manager = database_context(db_url)
    # here come the mysterious part that makes all subcommands
    # run inside the connection manager

@main.command
def do_this_thing()
    print('doing this thing')

@main.command
def do_that_thing()
    print('doing that thing')

这将被称为:

> that_cli do_that_thing
running command against local database
setup db connection: db://user:pass@localdb:db_name
doing that thing
teardown db connection

> that_cli --db staging do_this_thing
running command against staging database
setup db connection: db://user:pass@123.456.123.789:db_name
doing this thing
teardown db connection

编辑:请注意,上面的示例是伪造的,目的是更好地说明click缺少的功能,而不是我要特别解决此问题。我知道我可以在所有命令中重复相同的代码并达到相同的效果,这在我的实际用例中已经做到了。我的问题恰恰是关于我只能在主要功能中做什么,它将在上下文管理器中运行所有透明子命令。

1 个答案:

答案 0 :(得分:3)

装饰命令

  1. 使用contextlib.ContextDecorator
  2. 定义上下文管理器 decorator
  3. main()上使用click.pass_context装饰器,以便您可以探索点击上下文
  4. 创建上下文管理器的实例db_context
  5. 使用main遍历为组ctx.command.commands定义的命令
  6. 对于每个命令,将原始回调(由该命令调用的函数)替换为由上下文管理器db_context(cmd)装饰的同一回调。

这样,您将以编程方式修改每个命令,使其行为类似于:

@main.command()
@db_context
def do_this_thing():
    print('doing this thing')

但是无需在功能main()之外更改代码。

有关工作示例,请参见以下代码:

import click
from contextlib import ContextDecorator


class Database_context(ContextDecorator):
    """Decorator context manager."""

    def __init__(self, db_url):
        self.db_url = db_url

    def __enter__(self):
        print(f'setup db connection: {self.db_url}')

    def __exit__(self, type, value, traceback):
        print('teardown db connection')


@click.group() 
@click.option('--db', default='local')
@click.pass_context
def main(ctx, db):

    print(f'running command against {db} database')
    db_url = db  # get_db_url(db)

# here come the mysterious part that makes all subcommands
# run inside the connection manager

    db_context = Database_context(db_url)           # Init context manager decorator
    for name, cmd in ctx.command.commands.items():  # Iterate over main.commands
        cmd.allow_extra_args = True                 # Seems to be required, not sure why
        cmd.callback = db_context(cmd.callback)     # Decorate command callback with context manager


@main.command()
def do_this_thing():
    print('doing this thing')


@main.command()
def do_that_thing():
    print('doing that thing')


if __name__ == "__main__":
    main()

它可以完成您在问题中描述的内容,希望它可以按实际代码中的预期运行。


使用click.pass_context

下面的代码将为您提供如何使用click.pass_context进行操作的想法。

import click
from contextlib import contextmanager

@contextmanager
def database_context(db_url):
    try:
        print(f'setup db connection: {db_url}')
        yield
    finally:
        print('teardown db connection')


@click.group()
@click.option('--db',default='local')
@click.pass_context
def main(ctx, db):
    ctx.ensure_object(dict)
    print(f'running command against {db} database')
    db_url = db #get_db_url(db)
    # Initiate context manager
    ctx.obj['context'] = database_context(db_url)

@main.command()
@click.pass_context
def do_this_thing(ctx):
    with ctx.obj['context']:
        print('doing this thing')

@main.command()
@click.pass_context
def do_that_thing(ctx):
    with ctx.obj['context']:
        print('doing that thing')

if __name__ == "__main__":
    main(obj={})

另一个避免显式with语句的解决方案可以使用contextlib.ContextDecorator将上下文管理器作为装饰器传递,但是使用click进行设置可能会更复杂。