Skip to content

Instantly share code, notes, and snippets.

@danbradham
Created February 21, 2018 21:58
Show Gist options
  • Save danbradham/962db0b4b0b8ade28fb096d01816431d to your computer and use it in GitHub Desktop.
Save danbradham/962db0b4b0b8ade28fb096d01816431d to your computer and use it in GitHub Desktop.
Make Armin Ronacher's excellent click library even more excellent.
# -*- coding: utf-8 -*-
'''
extraclick.py
==============
Make Armin Ronacher's excellent click library even more excellent.
- ListOption: click.Option subclass with a custom parser that eats all
arguments until the next option is found. This allows for the same sort
of behavior as argparses' nargs=*.
.. code-block:: python
>>> @click.option('--values', '-v', cls=ListOption)
- OBJECT: click.ParamType subclass that uses ast.literal_eval to convert
arguments to python objects in the same manner as Python Fire.
.. code-block:: python
>>> @click.argument('obj', type=OBJECT)
'''
from __future__ import absolute_import, division, print_function
import click
import ast
class NameToString(ast.NodeTransformer):
'''Visits Name nodes and transforms them to Str nodes'''
def visit_Name(self, node):
if node.id in ('True', 'False', 'None'):
return node
return ast.copy_location(ast.Str(node.id), node)
def safe_eval(string):
'''Safely evaluates "string", converting it to a valid python object.
This is the same technique used by Python Fire to evaluate arguments. This
method is intended to be used for cli argument conversion and therefore
converts all ast.Name nodes to ast.Str nodes to reduce the need for
nested quotes. Note in the *Conversions* section, "{a: 1.0}" and
"{'a': 1.0}" both evaluate to {'a': 1.0}.
Arguments:
string (str): String to convert to a python object
Conversions:
"1" => 1
"1.0" => 1.0
"(1,)" => (1,)
"string" => 'string'
"{a: 1.0}" => {'a': 1.0}
"{'a': 1.0}" => {'a': 1.0}
"[a, 1, 2.0]" => ['a', 1, 2.0]
'''
tree = NameToString().visit(ast.parse(string, mode='eval'))
value = ast.literal_eval(tree)
return value
class ObjType(click.ParamType):
'''ObjType converts a command line arguments to a python objects using
ast.literal_eval. For convenience use the global OBJECT instead of
ObjType() with click.argument or click.option.
See also:
safe_eval, click.ParamType
Examples:
>>> @click.command()
... @click.argument('obj', type=OBJECT)
... def show_obj(obj):
... print(type(obj), obj)
'''
name = 'object'
def convert(self, value, param, ctx):
try:
value = safe_eval(value)
except:
self.fail('%s is not a valid python object' % value, param, ctx)
else:
return value
# Use me with the type keyword argument of click.argument and click.option
OBJECT = ObjType()
class ListOption(click.Option):
'''An Option that gets all values up until the next cli option and passes
them as a list. ast.literal_eval is used to convert the options arguments
to python objects.
See also:
click.Option
Examples:
>>> @click.command()
... @click.option('--keys', '-k', cls=ListOption)
... @click.option('--other', '-o')
... def show_keys(keys, other):
... print(keys)
'''
def add_to_parser(self, parser, ctx):
result = super(ListOption, self).add_to_parser(parser, ctx)
name = self.opts[0]
self._parser = parser._long_opt.get(name, parser._short_opt.get(name))
self._process = self._parser.process
def process(value, state):
value = [safe_eval(value)]
while state.rargs:
next_value = state.rargs[0]
for prefix in self._parser.prefixes:
if next_value.startswith(prefix):
break
else:
state.rargs.pop(0)
value.append(safe_eval(next_value))
return self._process(value, state)
self._parser.process = process
return result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment