From e77783dcd9b9b74f0f53c3b5857407cef07fba87 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 May 2014 13:44:42 +0200 Subject: Added support for default overrides. This fixes #42 --- click/core.py | 23 ++++++++++++++++++++++- docs/clickdoctools.py | 5 ++--- docs/commands.rst | 47 +++++++++++++++++++++++++++++++++++++++++++++++ tests/conftest.py | 5 +++-- tests/test_commands.py | 18 ++++++++++++++++++ 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/click/core.py b/click/core.py index 3d1238b..1cc1898 100644 --- a/click/core.py +++ b/click/core.py @@ -47,10 +47,12 @@ class Context(object): from environment variables is disabled. This does not affect manually set environment variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. """ def __init__(self, command, parent=None, info_name=None, obj=None, - auto_envvar_prefix=None): + auto_envvar_prefix=None, default_map=None): #: the parent context or `None` if none exists. self.parent = parent #: the :class:`Command` for this context. @@ -72,6 +74,8 @@ class Context(object): obj = parent.obj #: the user object stored. self.obj = obj + #: A dictionary (like object) with defaults for parameters. + self.default_map = default_map # If there is no envvar prefix yet, but the parent has one and # the command on this level has a name, we can expand the envvar @@ -149,6 +153,16 @@ class Context(object): self.obj = rv = object_type() return rv + def lookup_default(self, name): + """Looks up the default for a parameter name. This by default + looks into the :attr:`default_map` if available. + """ + if self.default_map is not None: + rv = self.default_map.get(name) + if callable(rv): + rv = rv() + return rv + def fail(self, message): """Aborts the execution of the program with a specific error message. @@ -362,6 +376,11 @@ class Command(object): :param extra: extra keyword arguments forwarded to the context constructor. """ + if 'default_map' not in extra: + default_map = None + if parent is not None and parent.default_map is not None: + default_map = parent.default_map.get(info_name) + extra['default_map'] = default_map ctx = Context(self, info_name=info_name, parent=parent, **extra) parser = self.make_parser(ctx) opts, args = parser.parse_args(args=args) @@ -701,6 +720,8 @@ class Parameter(object): def consume_value(self, ctx, opts): value = opts.get(self.name) + if value is None: + value = ctx.lookup_default(self.name) if value is None: value = self.value_from_envvar(ctx) return value diff --git a/docs/clickdoctools.py b/docs/clickdoctools.py index 11488f8..b46841e 100644 --- a/docs/clickdoctools.py +++ b/docs/clickdoctools.py @@ -132,7 +132,7 @@ class ExampleRunner(object): def invoke(cmd, args=None, prog_name=None, prog_prefix='python ', input=None, terminate_input=False, env=None, - auto_envvar_prefix=None): + **extra): if env: for key, value in sorted(env.items()): if ' ' in value: @@ -152,8 +152,7 @@ class ExampleRunner(object): input += '\xff' with isolation(input=input, env=env) as output: try: - cmd.main(args=args, prog_name=prog_name, - auto_envvar_prefix=auto_envvar_prefix) + cmd.main(args=args, prog_name=prog_name, **extra) except SystemExit: pass buffer.extend(output.getvalue().splitlines()) diff --git a/docs/commands.rst b/docs/commands.rst index 38e37b1..e4a12cd 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -254,3 +254,50 @@ And what it looks like: invoke(cli, prog_name='cli', args=['--help']) In case a command exists on more than one source, the first source wins. + +Overriding Defaults +------------------- + +By default the default value for a parameter is pulled from the +``default`` flag that is provided when it's defined. But that's not the +only place defaults can be loaded from. The other place is the +:attr:`Context.default_map` (a dictionary) on the context. This allows +defaults to be loaded from a config file to override the regular defaults. + +This is useful if you plug in some commands from another package but +you're not satisfied with the defaults. + +The default map can be nested arbitrarily for each subcommand and be +provided when the script is invoked. Alternatively it can also be +overriden at any point by commands. For instance a toplevel command could +load the defaults from a config file. + +Example usage: + +.. click:example:: + + import click + + @click.group() + def cli(): + pass + + @cli.command() + @click.option('--port', default=8000) + def runserver(port): + click.echo('Serving on http://127.0.0.1:%d/' % port) + + if __name__ == '__main__': + cli(default_map={ + 'runserver': { + 'port': 5000 + } + }) + +.. click:run:: + + invoke(cli, prog_name='cli', args=['runserver'], default_map={ + 'runserver': { + 'port': 5000 + } + }) diff --git a/tests/conftest.py b/tests/conftest.py index 1d2e0b9..c46b982 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -118,13 +118,14 @@ class CliRunner(object): click.termui.visible_prompt_func = old_visible_prompt_func click.termui.hidden_prompt_func = old_hidden_prompt_func - def invoke(self, cli, args): + def invoke(self, cli, args, **extra): with self.isolation() as out: exception = None exit_code = 0 try: - cli.main(args=args, prog_name=cli.name or 'root') + cli.main(args=args, prog_name=cli.name or 'root', + **extra) except SystemExit as e: if e.code != 0: exception = e diff --git a/tests/test_commands.py b/tests/test_commands.py index 03a4e10..9194991 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -38,3 +38,21 @@ def test_auto_shorthelp(runner): r'Commands:\n\s+' r'long\s+This is a long text that is too long to show\.\.\.\n\s+' r'short\s+This is a short text\.', result.output) is not None + + +def test_default_maps(runner): + @click.group() + def cli(): + pass + + @cli.command() + @click.option('--name', default='normal') + def foo(name): + click.echo(name) + + result = runner.invoke(cli, ['foo'], default_map={ + 'foo': {'name': 'changed'} + }) + + assert not result.exception + assert result.output == 'changed\n' -- cgit v1.2.1