diff --git a/cloudinary_cli/utils/utils.py b/cloudinary_cli/utils/utils.py index a07f65e..36f1d3f 100644 --- a/cloudinary_cli/utils/utils.py +++ b/cloudinary_cli/utils/utils.py @@ -7,6 +7,7 @@ from functools import reduce from hashlib import md5 from inspect import signature, getfullargspec +from typing import get_type_hints from multiprocessing import pool import click @@ -14,6 +15,7 @@ from jinja2 import Environment, FileSystemLoader from docstring_parser import parse from cloudinary_cli.defaults import logger, TEMPLATE_FOLDER +from cloudinary.utils import build_array not_callable = ('is_appengine_sandbox', 'call_tags_api', 'call_context_api', 'call_cacheable_api', 'call_api', 'call_metadata_api', 'call_json_api', 'only', 'transformation_string', 'account_config', @@ -127,9 +129,11 @@ def parse_args_kwargs(func, params=None, kwargs=None): num_req = num_args - num_defaults num_provided_args = len(params) num_overall_provided = num_provided_args + len([p for p in kwargs.keys() if p in spec.args[num_provided_args:]]) + if num_overall_provided < num_req: func_sig = signature(func) raise Exception(f"Function '{func.__name__}{func_sig}' requires {num_req} positional arguments") + # consume required args args = [parse_option_value(p) for p in params[:num_req]] @@ -142,8 +146,27 @@ def parse_args_kwargs(func, params=None, kwargs=None): k, v = p.split('=', 1) kwargs[k] = parse_option_value(v) + params_specs = parse(func.__doc__).params + + if len(args) > num_req: + # Here we comsumed more args than the function can get, + # let's see if we have a list arg and pass everything as list. + # Otherwise, let's pass everything as is and hope for the best :) + last_positional_list_param = next((s for s in reversed(params_specs) if s.arg_name not in kwargs and s.type_name and s.type_name.startswith('list')), None) + if last_positional_list_param: + pos = get_index_by_name(spec.args, last_positional_list_param.arg_name) + args[pos] = [args[pos]] + args[num_args:] + args = args[:num_args] + + for s in params_specs: + if s.type_name and s.type_name.startswith('list'): + pos = get_index_by_name(spec.args, s.arg_name) + args[pos] = normalize_list_params(args[pos]) + return args, kwargs +def get_index_by_name(lst, name): + return next((i for i, item in enumerate(lst) if item == name), -1) def remove_string_prefix(string, prefix): return string[string.startswith(prefix) and len(prefix):] @@ -297,17 +320,20 @@ def normalize_list_params(params): """ Normalizes parameters that could be provided as strings separated by ','. - >>> normalize_list_params(["f1,f2", "f3"]) - ["f1", "f2", "f3"] + >>> normalize_list_params(['f1,f2', 'f3']) + ['f1', 'f2', 'f3'] + + >>> normalize_list_params('f1,f2,f3') + ['f1', 'f2', 'f3'] :param params: Params to normalize. - :type params: list + :type params: list[string] or string :return: A list of normalized params. :rtype list """ normalized_params = [] - for f in list(params): + for f in build_array(params): if "," in f: normalized_params += f.split(",") else: diff --git a/test/test_utils.py b/test/test_utils.py index 3a8b1d2..3ed15f1 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -14,7 +14,7 @@ def test_parse_option_value(self): self.assertDictEqual({"foo": "bar"}, parse_option_value('{"foo":"bar"}')) self.assertDictEqual({"an": "object", "or": "dict"}, parse_option_value('{"an":"object","or":"dict"}')) self.assertListEqual( - ["this", "will", "be", "read", "as","a", "list"], + ["this", "will", "be", "read", "as", "a", "list"], parse_option_value('["this","will","be","read","as","a","list"]') ) self.assertListEqual( @@ -62,6 +62,14 @@ def test_parse_args_kwargs(self): self.assertEqual(0, len(args)) self.assertDictEqual({"arg1": "a1", "arg2": "a2"}, kwargs) + # should consume list values separated by spaces and commas + args, kwargs = parse_args_kwargs(_list_args_test_func, ["l0a0,l0a1,l0a2", "sa0", "l1a0", "sa2", "l1a1,l1a2", "l1a3"]) + self.assertEqual(4, len(args)) + self.assertListEqual(["l0a0", "l0a1", "l0a2"], args[0]) + self.assertEqual("sa0", args[1]) + self.assertListEqual(["l1a0", "l1a1", "l1a2", "l1a3"], args[2]) + self.assertEqual("sa2", args[3]) + def test_group_params(self): self.assertDictEqual({}, group_params([])) self.assertDictEqual({"k1": "v1", "k2": "v2"}, group_params([("k1", "v1"), ("k2", "v2")])) @@ -113,7 +121,9 @@ def test_merge_responses(self): def test_normalize_list_params(self): """ should normalize a list of parameters """ - self.assertEqual(["f1", "f2", "f3"], normalize_list_params(["f1,f2", "f3"])) + self.assertListEqual(["f1"], normalize_list_params("f1")) + self.assertListEqual(["f1", "f2", "f3"], normalize_list_params(["f1,f2", "f3"])) + self.assertListEqual(["f1", "f2", "f3"], normalize_list_params("f1,f2,f3")) def test_chunker(self): animals = ['cat', 'dog', 'rabbit', 'duck', 'bird', 'cow', 'gnu', 'fish'] @@ -131,3 +141,21 @@ def _only_args_test_func(arg1, arg2): def _args_kwargs_test_func(arg1, arg2=None): return arg1, arg2 + + +def _list_args_test_func(fist_list_arg, non_list_arg, list_arg, non_list_arg2): + """ + Function for testing list args. + + :param fist_list_arg: first list argument + :type fist_list_arg: list + :param non_list_arg: some non-list argument + :type non_list_arg: str + :param list_arg: some list argument + :type list_arg: list + :param non_list_arg2: another non-list argument + :type non_list_arg2: str + :return: tuple of arguments + :rtype: tuple + """ + return fist_list_arg, non_list_arg, list_arg, non_list_arg2