From 6c555e62aa760d969e71420a7f464fbfba38f2ae Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 26 Aug 2018 03:02:58 +0200 Subject: [PATCH 1/2] Refactor argument clinic usage --- jedi/evaluate/arguments.py | 95 +++++++++++++++++++++++++++++--------- jedi/evaluate/stdlib.py | 54 +++++++--------------- 2 files changed, 90 insertions(+), 59 deletions(-) diff --git a/jedi/evaluate/arguments.py b/jedi/evaluate/arguments.py index d0a06224..e8c8a763 100644 --- a/jedi/evaluate/arguments.py +++ b/jedi/evaluate/arguments.py @@ -1,3 +1,5 @@ +import re + from parso.python import tree from jedi._compatibility import zip_longest @@ -28,32 +30,83 @@ def try_iter_content(types, depth=0): try_iter_content(lazy_context.infer(), depth + 1) +def repack_with_argument_clinic(string, keep_arguments_param=False): + """ + Transforms a function or method with arguments to the signature that is + given as an argument clinic notation. + + Argument clinic is part of CPython and used for all the functions that are + implemented in C (Python 3.7): + + >>> str.split.__text_signature__ + '($self, /, sep=None, maxsplit=-1)' + """ + clinic_args = list(_parse_argument_clinic(string)) + + def decorator(func): + def wrapper(*args, **kwargs): + if keep_arguments_param: + arguments = kwargs['arguments'] + else: + arguments = kwargs.pop('arguments') + try: + args += tuple(_iterate_argument_clinic(arguments, clinic_args)) + except ValueError: + return NO_CONTEXTS + else: + return func(*args, **kwargs) + + return wrapper + return decorator + + +def _iterate_argument_clinic(arguments, parameters): + """Uses a list with argument clinic information (see PEP 436).""" + iterator = arguments.unpack() + for i, (name, optional, allow_kwargs) in enumerate(parameters): + key, argument = next(iterator, (None, None)) + if key is not None: + debug.warning('Keyword arguments in argument clinic are currently not supported.') + raise ValueError + if argument is None and not optional: + debug.warning('TypeError: %s expected at least %s arguments, got %s', + name, len(parameters), i) + raise ValueError + + context_set = NO_CONTEXTS if argument is None else argument.infer() + + if not context_set and not optional: + # For the stdlib we always want values. If we don't get them, + # that's ok, maybe something is too hard to resolve, however, + # we will not proceed with the evaluation of that function. + debug.warning('argument_clinic "%s" not resolvable.', name) + raise ValueError + yield context_set + + +def _parse_argument_clinic(string): + allow_kwargs = False + optional = False + while string: + # Optional arguments have to begin with a bracket. And should always be + # at the end of the arguments. This is therefore not a proper argument + # clinic implementation. `range()` for exmple allows an optional start + # value at the beginning. + match = re.match('(?:(?:(\[),? ?|, ?|)(\w+)|, ?/)\]*', string) + string = string[len(match.group(0)):] + if not match.group(2): # A slash -> allow named arguments + allow_kwargs = True + continue + optional = optional or bool(match.group(1)) + word = match.group(2) + yield (word, optional, allow_kwargs) + + class AbstractArguments(object): context = None argument_node = None trailer = None - def eval_argument_clinic(self, parameters): - """Uses a list with argument clinic information (see PEP 436).""" - iterator = self.unpack() - for i, (name, optional, allow_kwargs) in enumerate(parameters): - key, argument = next(iterator, (None, None)) - if key is not None: - raise NotImplementedError - if argument is None and not optional: - debug.warning('TypeError: %s expected at least %s arguments, got %s', - name, len(parameters), i) - raise ValueError - values = NO_CONTEXTS if argument is None else argument.infer() - - if not values and not optional: - # For the stdlib we always want values. If we don't get them, - # that's ok, maybe something is too hard to resolve, however, - # we will not proceed with the evaluation of that function. - debug.warning('argument_clinic "%s" not resolvable.', name) - raise ValueError - yield values - def eval_all(self, funcdef=None): """ Evaluates all arguments as a support for static analysis diff --git a/jedi/evaluate/stdlib.py b/jedi/evaluate/stdlib.py index 3ca38322..52c22383 100644 --- a/jedi/evaluate/stdlib.py +++ b/jedi/evaluate/stdlib.py @@ -9,13 +9,11 @@ Note that this module exists only to implement very specific functionality in the standard library. The usual way to understand the standard library is the compiled module that returns the types for C-builtins. """ -import re - import parso from jedi._compatibility import force_unicode from jedi import debug -from jedi.evaluate.arguments import ValuesArguments +from jedi.evaluate.arguments import ValuesArguments, repack_with_argument_clinic from jedi.evaluate import analysis from jedi.evaluate import compiled from jedi.evaluate.context.instance import \ @@ -71,7 +69,7 @@ def execute(evaluator, obj, arguments): except KeyError: pass else: - return func(evaluator, obj, arguments) + return func(evaluator, obj, arguments=arguments) raise NotInStdLib() @@ -88,42 +86,22 @@ def argument_clinic(string, want_obj=False, want_context=False, want_arguments=F """ Works like Argument Clinic (PEP 436), to validate function params. """ - clinic_args = [] - allow_kwargs = False - optional = False - while string: - # Optional arguments have to begin with a bracket. And should always be - # at the end of the arguments. This is therefore not a proper argument - # clinic implementation. `range()` for exmple allows an optional start - # value at the beginning. - match = re.match('(?:(?:(\[),? ?|, ?|)(\w+)|, ?/)\]*', string) - string = string[len(match.group(0)):] - if not match.group(2): # A slash -> allow named arguments - allow_kwargs = True - continue - optional = optional or bool(match.group(1)) - word = match.group(2) - clinic_args.append((word, optional, allow_kwargs)) def f(func): - def wrapper(evaluator, obj, arguments): + @repack_with_argument_clinic(string, keep_arguments_param=True) + def wrapper(evaluator, obj, *args, **kwargs): + arguments = kwargs.pop('arguments') + assert not kwargs # Python 2... debug.dbg('builtin start %s' % obj, color='MAGENTA') result = NO_CONTEXTS - try: - lst = list(arguments.eval_argument_clinic(clinic_args)) - except ValueError: - pass - else: - kwargs = {} - if want_context: - kwargs['context'] = arguments.context - if want_obj: - kwargs['obj'] = obj - if want_arguments: - kwargs['arguments'] = arguments - result = func(evaluator, *lst, **kwargs) - finally: - debug.dbg('builtin end: %s', result, color='MAGENTA') + if want_context: + kwargs['context'] = arguments.context + if want_obj: + kwargs['obj'] = obj + if want_arguments: + kwargs['arguments'] = arguments + result = func(evaluator, *args, **kwargs) + debug.dbg('builtin end: %s', result, color='MAGENTA') return result return wrapper @@ -334,8 +312,8 @@ _implemented = { 'deepcopy': _return_first_param, }, 'json': { - 'load': lambda *args: NO_CONTEXTS, - 'loads': lambda *args: NO_CONTEXTS, + 'load': lambda evaluator, obj, arguments: NO_CONTEXTS, + 'loads': lambda evaluator, obj, arguments: NO_CONTEXTS, }, 'collections': { 'namedtuple': collections_namedtuple, From 84b07a88091c31e55a3a3886412d30a9929ae406 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 26 Aug 2018 03:09:46 +0200 Subject: [PATCH 2/2] Removing a test from doctests, becaues it shouldn't be one --- jedi/evaluate/arguments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/evaluate/arguments.py b/jedi/evaluate/arguments.py index e8c8a763..a1320fbb 100644 --- a/jedi/evaluate/arguments.py +++ b/jedi/evaluate/arguments.py @@ -38,8 +38,8 @@ def repack_with_argument_clinic(string, keep_arguments_param=False): Argument clinic is part of CPython and used for all the functions that are implemented in C (Python 3.7): - >>> str.split.__text_signature__ - '($self, /, sep=None, maxsplit=-1)' + str.split.__text_signature__ + # Results in: '($self, /, sep=None, maxsplit=-1)' """ clinic_args = list(_parse_argument_clinic(string))