From 39f1dfc85e6deacdacd020f4d18bb162618294f4 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 3 Sep 2018 09:50:45 +0200 Subject: [PATCH] WIP of namedtuple/itemgetter/property --- jedi/evaluate/arguments.py | 30 ++++++++++++--- jedi/evaluate/base_context.py | 11 ++++++ jedi/evaluate/context/instance.py | 10 ++--- jedi/plugins/stdlib.py | 61 +++++++++++++++++++++++++++---- test/completion/stdlib.py | 12 ++++++ 5 files changed, 106 insertions(+), 18 deletions(-) diff --git a/jedi/evaluate/arguments.py b/jedi/evaluate/arguments.py index d464418b..1ffcac2b 100644 --- a/jedi/evaluate/arguments.py +++ b/jedi/evaluate/arguments.py @@ -4,12 +4,12 @@ from parso.python import tree from jedi._compatibility import zip_longest from jedi import debug -from jedi.evaluate.utils import to_list +from jedi.evaluate.utils import to_list, PushBackIterator from jedi.evaluate import analysis from jedi.evaluate.lazy_context import LazyKnownContext, LazyKnownContexts, \ LazyTreeContext, get_merged_lazy_context from jedi.evaluate.filters import ParamName -from jedi.evaluate.base_context import NO_CONTEXTS +from jedi.evaluate.base_context import NO_CONTEXTS, ContextSet from jedi.evaluate.context import iterable from jedi.evaluate.param import get_executed_params, ExecutedParam @@ -63,9 +63,23 @@ def repack_with_argument_clinic(string, keep_arguments_param=False): 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): + iterator = PushBackIterator(arguments.unpack()) + for i, (name, optional, allow_kwargs, stars) in enumerate(parameters): + if stars == 1: + lazy_contexts = [] + for key, argument in iterator: + if key is not None: + iterator.push_back((key, argument)) + break + + lazy_contexts.append(argument) + yield ContextSet(iterable.FakeSequence(evaluator, u'tuple', lazy_contexts)) + lazy_contexts + continue + elif stars == 2: + raise NotImplementedError() key, argument = next(iterator, (None, None)) + print(stars) if key is not None: debug.warning('Keyword arguments in argument clinic are currently not supported.') raise ValueError @@ -93,14 +107,18 @@ def _parse_argument_clinic(string): # 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) + 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) + stars = word.count('*') + word = word[stars:] + yield (word, optional, allow_kwargs, stars) + if stars: + allow_kwargs = True class AbstractArguments(object): diff --git a/jedi/evaluate/base_context.py b/jedi/evaluate/base_context.py index 5ba7d1dd..44603b7d 100644 --- a/jedi/evaluate/base_context.py +++ b/jedi/evaluate/base_context.py @@ -122,6 +122,17 @@ def iterate_contexts(contexts, contextualized_node=None, is_async=False): ) +class ContextWrapper(object): + def __init__(self, wrapped_context): + self._wrapped_context = wrapped_context + + def __getattr__(self, name): + return getattr(self._wrapped_context, name) + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, self._wrapped_context) + + class TreeContext(Context): def __init__(self, evaluator, parent_context, tree_node): super(TreeContext, self).__init__(evaluator, parent_context) diff --git a/jedi/evaluate/context/instance.py b/jedi/evaluate/context/instance.py index 2bd2053a..faac655d 100644 --- a/jedi/evaluate/context/instance.py +++ b/jedi/evaluate/context/instance.py @@ -318,8 +318,8 @@ class BoundMethod(AbstractFunction): function.parent_context, function.tree_node, ) - self._instance = instance - self._class = klass + self.instance = instance + self.class_context = klass self._function = function def py__class__(self): @@ -327,9 +327,9 @@ class BoundMethod(AbstractFunction): def _get_arguments(self, arguments): if arguments is None: - arguments = AnonymousInstanceArguments(self._instance) + arguments = AnonymousInstanceArguments(self.instance) - return InstanceArguments(self._instance, arguments) + return InstanceArguments(self.instance, arguments) def get_function_execution(self, arguments=None): arguments = self._get_arguments(arguments) @@ -344,7 +344,7 @@ class BoundMethod(AbstractFunction): return self._function.get_function_execution(arguments) def get_default_param_context(self): - return self._class + return self.class_context def py__call__(self, arguments): if isinstance(self._function, OverloadedFunctionContext): diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 23c590c7..5148f608 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -15,13 +15,13 @@ from jedi._compatibility import force_unicode from jedi.plugins.base import BasePlugin from jedi import debug from jedi.evaluate.arguments import ValuesArguments, \ - repack_with_argument_clinic, AbstractArguments + repack_with_argument_clinic, AbstractArguments, TreeArguments from jedi.evaluate import analysis from jedi.evaluate import compiled from jedi.evaluate.context.instance import \ AbstractInstanceContext, CompiledInstance, BoundMethod, InstanceArguments from jedi.evaluate.base_context import ContextualizedNode, \ - NO_CONTEXTS, ContextSet + NO_CONTEXTS, ContextSet, ContextWrapper from jedi.evaluate.context import ClassContext, ModuleContext, \ FunctionExecutionContext from jedi.evaluate.context import iterable @@ -49,9 +49,6 @@ _NAMEDTUPLE_INIT = """ class StdlibPlugin(BasePlugin): def execute(self, callback): def wrapper(context, arguments): - if isinstance(context, BoundMethod): - return callback(context, arguments=arguments) - debug.dbg('execute: %s %s', context, arguments) try: obj_name = context.name.string_name @@ -65,6 +62,16 @@ class StdlibPlugin(BasePlugin): else: module_name = '' + if isinstance(context, BoundMethod): + if module_name == 'builtins' and context.py__name__() == '__get__': + if context.class_context.py__name__() == 'property': + return builtins_property( + self._evaluator, + context, + arguments=arguments + ) + return callback(context, arguments=arguments) + # for now we just support builtin functions. try: func = _implemented[module_name][obj_name] @@ -112,6 +119,21 @@ def argument_clinic(string, want_obj=False, want_context=False, want_arguments=F return f +@argument_clinic('obj, type, /', want_obj=True, want_arguments=True) +def builtins_property(evaluator, objects, types, obj, arguments): + print(obj) + print(obj.instance.var_args) + property_args = obj.instance.var_args.unpack() + key, lazy_context = next(property_args, (None, None)) + if key is not None or lazy_context is None: + debug.warning('property expected a first param, not %s', arguments) + return NO_CONTEXTS + print('lazy_context', lazy_context, lazy_context.infer()) + print() + print(objects, type) + return NO_CONTEXTS + + @argument_clinic('iterator[, default], /') def builtins_next(evaluator, iterators, defaults): """ @@ -291,8 +313,12 @@ def collections_namedtuple(evaluator, obj, arguments): # Parse source code module = evaluator.grammar.parse(code) generated_class = next(module.iter_classdefs()) - parent_context = None - raise NotImplementedError('TODO implement parent_context') + parent_context = ModuleContext( + evaluator, module, + path=None, + string_names=None, + code_lines=parso.split_lines(code, keepends=True), + ) return ContextSet(ClassContext(evaluator, parent_context, generated_class)) @@ -353,6 +379,24 @@ def _random_choice(evaluator, sequences): ) +class ItemGetterCallable(object): + def __init__(self, args_context_set): + # TODO this context is totally incomplete and will raise exceptions. + self._args_context_set = args_context_set + + @repack_with_argument_clinic('item, /') + def py__call__(self, item): + return self._args_context_set.py__getitem__(item) + + +@argument_clinic('*args, /', want_obj=True, want_arguments=True) +def _operator_itemgetter(evaluator, args_context_set, obj, arguments): + final = obj.py__call__(arguments) + print(final) + return final + return ItemGetterCallable(args_context_set) + + _implemented = { 'builtins': { 'getattr': builtins_getattr, @@ -382,6 +426,9 @@ _implemented = { 'random': { 'choice': _random_choice, }, + 'operator': { + 'itemgetter': _operator_itemgetter, + }, 'abc': { # Not sure if this is necessary, but it's used a lot in typeshed and # it's for now easier to just pass the function. diff --git a/test/completion/stdlib.py b/test/completion/stdlib.py index b9c53960..38401ffc 100644 --- a/test/completion/stdlib.py +++ b/test/completion/stdlib.py @@ -241,6 +241,18 @@ with contextlib.closing('asd') as string: #? str() string +# ----------------- +# operator +# ----------------- + +import operator + +f = operator.itemgetter([1]) +#? float() +f([1.0]) +#? str() +f([1, '']) + # ----------------- # shlex # -----------------