from abc import abstractmethod from contextlib import contextmanager from pathlib import Path from typing import Optional from parso.tree import search_ancestor from parso.python.tree import Name from jedi.inference.filters import ParserTreeFilter, MergedFilter, \ GlobalNameFilter from jedi.inference.names import AnonymousParamName, TreeNameDefinition from jedi.inference.base_value import NO_VALUES, ValueSet from jedi.parser_utils import get_parent_scope from jedi import debug from jedi import parser_utils class AbstractContext: # Must be defined: inference_state and tree_node and parent_context as an attribute/property def __init__(self, inference_state): self.inference_state = inference_state self.predefined_names = {} @abstractmethod def get_filters(self, until_position=None, origin_scope=None): raise NotImplementedError def goto(self, name_or_str, position): from jedi.inference import finder filters = _get_global_filters_for_name( self, name_or_str if isinstance(name_or_str, Name) else None, position, ) names = finder.filter_name(filters, name_or_str) debug.dbg('context.goto %s in (%s): %s', name_or_str, self, names) return names def py__getattribute__(self, name_or_str, name_context=None, position=None, analysis_errors=True): """ :param position: Position of the last statement -> tuple of line, column """ if name_context is None: name_context = self names = self.goto(name_or_str, position) string_name = name_or_str.value if isinstance(name_or_str, Name) else name_or_str # This paragraph is currently needed for proper branch type inference # (static analysis). found_predefined_types = None if self.predefined_names and isinstance(name_or_str, Name): node = name_or_str while node is not None and not parser_utils.is_scope(node): node = node.parent if node.type in ("if_stmt", "for_stmt", "comp_for", 'sync_comp_for'): try: name_dict = self.predefined_names[node] types = name_dict[string_name] except KeyError: continue else: found_predefined_types = types break if found_predefined_types is not None and names: from jedi.inference import flow_analysis check = flow_analysis.reachability_check( context=self, value_scope=self.tree_node, node=name_or_str, ) if check is flow_analysis.UNREACHABLE: values = NO_VALUES else: values = found_predefined_types else: values = ValueSet.from_sets(name.infer() for name in names) if not names and not values and analysis_errors: if isinstance(name_or_str, Name): from jedi.inference import analysis message = ("NameError: name '%s' is not defined." % string_name) analysis.add(name_context, 'name-error', name_or_str, message) debug.dbg('context.names_to_types: %s -> %s', names, values) if values: return values return self._check_for_additional_knowledge(name_or_str, name_context, position) def _check_for_additional_knowledge(self, name_or_str, name_context, position): name_context = name_context or self # Add isinstance and other if/assert knowledge. if isinstance(name_or_str, Name) and not name_context.is_instance(): flow_scope = name_or_str base_nodes = [name_context.tree_node] if any(b.type in ('comp_for', 'sync_comp_for') for b in base_nodes): return NO_VALUES from jedi.inference.finder import check_flow_information while True: flow_scope = get_parent_scope(flow_scope, include_flows=True) n = check_flow_information(name_context, flow_scope, name_or_str, position) if n is not None: return n if flow_scope in base_nodes: break return NO_VALUES def get_root_context(self): parent_context = self.parent_context if parent_context is None: return self return parent_context.get_root_context() def is_module(self): return False def is_builtins_module(self): return False def is_class(self): return False def is_stub(self): return False def is_instance(self): return False def is_compiled(self): return False def is_bound_method(self): return False @abstractmethod def py__name__(self): raise NotImplementedError def get_value(self): raise NotImplementedError @property def name(self): return None def get_qualified_names(self): return () def py__doc__(self): return '' @contextmanager def predefine_names(self, flow_scope, dct): predefined = self.predefined_names predefined[flow_scope] = dct try: yield finally: del predefined[flow_scope] class ValueContext(AbstractContext): """ Should be defined, otherwise the API returns empty types. """ def __init__(self, value): super().__init__(value.inference_state) self._value = value @property def tree_node(self): return self._value.tree_node @property def parent_context(self): return self._value.parent_context def is_module(self): return self._value.is_module() def is_builtins_module(self): return self._value == self.inference_state.builtins_module def is_class(self): return self._value.is_class() def is_stub(self): return self._value.is_stub() def is_instance(self): return self._value.is_instance() def is_compiled(self): return self._value.is_compiled() def is_bound_method(self): return self._value.is_bound_method() def py__name__(self): return self._value.py__name__() @property def name(self): return self._value.name def get_qualified_names(self): return self._value.get_qualified_names() def py__doc__(self): return self._value.py__doc__() def get_value(self): return self._value def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self._value) class TreeContextMixin: def infer_node(self, node): from jedi.inference.syntax_tree import infer_node return infer_node(self, node) def create_value(self, node): from jedi.inference import value if node == self.tree_node: assert self.is_module() return self.get_value() parent_context = self.create_context(node) if node.type in ('funcdef', 'lambdef'): func = value.FunctionValue.from_context(parent_context, node) if parent_context.is_class(): class_value = parent_context.parent_context.create_value(parent_context.tree_node) instance = value.AnonymousInstance( self.inference_state, parent_context.parent_context, class_value) func = value.BoundMethod( instance=instance, class_context=class_value.as_context(), function=func ) return func elif node.type == 'classdef': return value.ClassValue(self.inference_state, parent_context, node) else: raise NotImplementedError("Probably shouldn't happen: %s" % node) def create_context(self, node): def from_scope_node(scope_node, is_nested=True): if scope_node == self.tree_node: return self if scope_node.type in ('funcdef', 'lambdef', 'classdef'): return self.create_value(scope_node).as_context() elif scope_node.type in ('comp_for', 'sync_comp_for'): parent_context = from_scope_node(parent_scope(scope_node.parent)) if node.start_pos >= scope_node.children[-1].start_pos: return parent_context return CompForContext(parent_context, scope_node) raise Exception("There's a scope that was not managed: %s" % scope_node) def parent_scope(node): while True: node = node.parent if parser_utils.is_scope(node): return node elif node.type in ('argument', 'testlist_comp'): if node.children[1].type in ('comp_for', 'sync_comp_for'): return node.children[1] elif node.type == 'dictorsetmaker': for n in node.children[1:4]: # In dictionaries it can be pretty much anything. if n.type in ('comp_for', 'sync_comp_for'): return n scope_node = parent_scope(node) if scope_node.type in ('funcdef', 'classdef'): colon = scope_node.children[scope_node.children.index(':')] if node.start_pos < colon.start_pos: parent = node.parent if not (parent.type == 'param' and parent.name == node): scope_node = parent_scope(scope_node) return from_scope_node(scope_node, is_nested=True) def create_name(self, tree_name): definition = tree_name.get_definition() if definition and definition.type == 'param' and definition.name == tree_name: funcdef = search_ancestor(definition, 'funcdef', 'lambdef') func = self.create_value(funcdef) return AnonymousParamName(func, tree_name) else: context = self.create_context(tree_name) return TreeNameDefinition(context, tree_name) class FunctionContext(TreeContextMixin, ValueContext): def get_filters(self, until_position=None, origin_scope=None): yield ParserTreeFilter( self.inference_state, parent_context=self, until_position=until_position, origin_scope=origin_scope ) class ModuleContext(TreeContextMixin, ValueContext): def py__file__(self) -> Optional[Path]: return self._value.py__file__() # type: ignore[no-any-return] def get_filters(self, until_position=None, origin_scope=None): filters = self._value.get_filters(origin_scope) # Skip the first filter and replace it. next(filters, None) yield MergedFilter( ParserTreeFilter( parent_context=self, until_position=until_position, origin_scope=origin_scope ), self.get_global_filter(), ) yield from filters def get_global_filter(self): return GlobalNameFilter(self) @property def string_names(self): return self._value.string_names @property def code_lines(self): return self._value.code_lines def get_value(self): """ This is the only function that converts a context back to a value. This is necessary for stub -> python conversion and vice versa. However this method shouldn't be moved to AbstractContext. """ return self._value class NamespaceContext(TreeContextMixin, ValueContext): def get_filters(self, until_position=None, origin_scope=None): return self._value.get_filters() def get_value(self): return self._value @property def string_names(self): return self._value.string_names def py__file__(self) -> Optional[Path]: return self._value.py__file__() # type: ignore[no-any-return] class ClassContext(TreeContextMixin, ValueContext): def get_filters(self, until_position=None, origin_scope=None): yield self.get_global_filter(until_position, origin_scope) def get_global_filter(self, until_position=None, origin_scope=None): return ParserTreeFilter( parent_context=self, until_position=until_position, origin_scope=origin_scope ) class CompForContext(TreeContextMixin, AbstractContext): def __init__(self, parent_context, comp_for): super().__init__(parent_context.inference_state) self.tree_node = comp_for self.parent_context = parent_context def get_filters(self, until_position=None, origin_scope=None): yield ParserTreeFilter(self) def get_value(self): return None def py__name__(self): return '' def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self.tree_node) class CompiledContext(ValueContext): def get_filters(self, until_position=None, origin_scope=None): return self._value.get_filters() class CompiledModuleContext(CompiledContext): code_lines = None def get_value(self): return self._value @property def string_names(self): return self._value.string_names def py__file__(self) -> Optional[Path]: return self._value.py__file__() # type: ignore[no-any-return] def _get_global_filters_for_name(context, name_or_none, position): # For functions and classes the defaults don't belong to the # function and get inferred in the value before the function. So # make sure to exclude the function/class name. if name_or_none is not None: ancestor = search_ancestor(name_or_none, 'funcdef', 'classdef', 'lambdef') lambdef = None if ancestor == 'lambdef': # For lambdas it's even more complicated since parts will # be inferred later. lambdef = ancestor ancestor = search_ancestor(name_or_none, 'funcdef', 'classdef') if ancestor is not None: colon = ancestor.children[-2] if position is not None and position < colon.start_pos: if lambdef is None or position < lambdef.children[-2].start_pos: position = ancestor.start_pos return get_global_filters(context, position, name_or_none) def get_global_filters(context, until_position, origin_scope): """ Returns all filters in order of priority for name resolution. For global name lookups. The filters will handle name resolution themselves, but here we gather possible filters downwards. >>> from jedi import Script >>> script = Script(''' ... x = ['a', 'b', 'c'] ... def func(): ... y = None ... ''') >>> module_node = script._module_node >>> scope = next(module_node.iter_funcdefs()) >>> scope >>> context = script._get_module_context().create_context(scope) >>> filters = list(get_global_filters(context, (4, 0), None)) First we get the names from the function scope. >>> print(filters[0]) # doctest: +ELLIPSIS MergedFilter(, ) >>> sorted(str(n) for n in filters[0].values()) # doctest: +NORMALIZE_WHITESPACE ['', ''] >>> filters[0]._filters[0]._until_position (4, 0) >>> filters[0]._filters[1]._until_position Then it yields the names from one level "lower". In this example, this is the module scope (including globals). As a side note, you can see, that the position in the filter is None on the globals filter, because there the whole module is searched. >>> list(filters[1].values()) # package modules -> Also empty. [] >>> sorted(name.string_name for name in filters[2].values()) # Module attributes ['__doc__', '__name__', '__package__'] Finally, it yields the builtin filter, if `include_builtin` is true (default). >>> list(filters[3].values()) # doctest: +ELLIPSIS [...] """ base_context = context from jedi.inference.value.function import BaseFunctionExecutionContext while context is not None: # Names in methods cannot be resolved within the class. yield from context.get_filters( until_position=until_position, origin_scope=origin_scope ) if isinstance(context, (BaseFunctionExecutionContext, ModuleContext)): # The position should be reset if the current scope is a function. until_position = None context = context.parent_context b = next(base_context.inference_state.builtins_module.get_filters(), None) assert b is not None # Add builtins to the global scope. yield b