Format: text/x-python
⬇ Download File (15.7 KB)
   1  """
   2  PEP 0484 ( https://www.python.org/dev/peps/pep-0484/ ) describes type hints
   3  through function annotations. There is a strong suggestion in this document
   4  that only the type of type hinting defined in PEP0484 should be allowed
   5  as annotations in future python versions.
   6  """
   7  
   8  import re
   9  from inspect import Parameter
  10  
  11  from parso import ParserSyntaxError, parse
  12  
  13  from jedi.inference.cache import inference_state_method_cache
  14  from jedi.inference.base_value import ValueSet, NO_VALUES
  15  from jedi.inference.gradual.base import DefineGenericBaseClass, GenericClass
  16  from jedi.inference.gradual.generics import TupleGenericManager
  17  from jedi.inference.gradual.type_var import TypeVar
  18  from jedi.inference.helpers import is_string
  19  from jedi.inference.compiled import builtin_from_name
  20  from jedi.inference.param import get_executed_param_names
  21  from jedi import debug
  22  from jedi import parser_utils
  23  
  24  
  25  def infer_annotation(context, annotation):
  26      """
  27      Inferes an annotation node. This means that it inferes the part of
  28      `int` here:
  29  
  30          foo: int = 3
  31  
  32      Also checks for forward references (strings)
  33      """
  34      value_set = context.infer_node(annotation)
  35      if len(value_set) != 1:
  36          debug.warning("Inferred typing index %s should lead to 1 object, "
  37                        " not %s" % (annotation, value_set))
  38          return value_set
  39  
  40      inferred_value = list(value_set)[0]
  41      if is_string(inferred_value):
  42          result = _get_forward_reference_node(context, inferred_value.get_safe_value())
  43          if result is not None:
  44              return context.infer_node(result)
  45      return value_set
  46  
  47  
  48  def _infer_annotation_string(context, string, index=None):
  49      node = _get_forward_reference_node(context, string)
  50      if node is None:
  51          return NO_VALUES
  52  
  53      value_set = context.infer_node(node)
  54      if index is not None:
  55          value_set = value_set.filter(
  56              lambda value: (
  57                  value.array_type == 'tuple'
  58                  and len(list(value.py__iter__())) >= index
  59              )
  60          ).py__simple_getitem__(index)
  61      return value_set
  62  
  63  
  64  def _get_forward_reference_node(context, string):
  65      try:
  66          new_node = context.inference_state.grammar.parse(
  67              string,
  68              start_symbol='eval_input',
  69              error_recovery=False
  70          )
  71      except ParserSyntaxError:
  72          debug.warning('Annotation not parsed: %s' % string)
  73          return None
  74      else:
  75          module = context.tree_node.get_root_node()
  76          parser_utils.move(new_node, module.end_pos[0])
  77          new_node.parent = context.tree_node
  78          return new_node
  79  
  80  
  81  def _split_comment_param_declaration(decl_text):
  82      """
  83      Split decl_text on commas, but group generic expressions
  84      together.
  85  
  86      For example, given "foo, Bar[baz, biz]" we return
  87      ['foo', 'Bar[baz, biz]'].
  88  
  89      """
  90      try:
  91          node = parse(decl_text, error_recovery=False).children[0]
  92      except ParserSyntaxError:
  93          debug.warning('Comment annotation is not valid Python: %s' % decl_text)
  94          return []
  95  
  96      if node.type in ['name', 'atom_expr', 'power']:
  97          return [node.get_code().strip()]
  98  
  99      params = []
 100      try:
 101          children = node.children
 102      except AttributeError:
 103          return []
 104      else:
 105          for child in children:
 106              if child.type in ['name', 'atom_expr', 'power']:
 107                  params.append(child.get_code().strip())
 108  
 109      return params
 110  
 111  
 112  @inference_state_method_cache()
 113  def infer_param(function_value, param, ignore_stars=False):
 114      values = _infer_param(function_value, param)
 115      if ignore_stars or not values:
 116          return values
 117      inference_state = function_value.inference_state
 118      if param.star_count == 1:
 119          tuple_ = builtin_from_name(inference_state, 'tuple')
 120          return ValueSet([GenericClass(
 121              tuple_,
 122              TupleGenericManager((values,)),
 123          )])
 124      elif param.star_count == 2:
 125          dct = builtin_from_name(inference_state, 'dict')
 126          generics = (
 127              ValueSet([builtin_from_name(inference_state, 'str')]),
 128              values
 129          )
 130          return ValueSet([GenericClass(
 131              dct,
 132              TupleGenericManager(generics),
 133          )])
 134      return values
 135  
 136  
 137  def _infer_param(function_value, param):
 138      """
 139      Infers the type of a function parameter, using type annotations.
 140      """
 141      annotation = param.annotation
 142      if annotation is None:
 143          # If no Python 3-style annotation, look for a comment annotation.
 144          # Identify parameters to function in the same sequence as they would
 145          # appear in a type comment.
 146          all_params = [child for child in param.parent.children
 147                        if child.type == 'param']
 148  
 149          node = param.parent.parent
 150          comment = parser_utils.get_following_comment_same_line(node)
 151          if comment is None:
 152              return NO_VALUES
 153  
 154          match = re.match(r"^#\s*type:\s*\(([^#]*)\)\s*->", comment)
 155          if not match:
 156              return NO_VALUES
 157          params_comments = _split_comment_param_declaration(match.group(1))
 158  
 159          # Find the specific param being investigated
 160          index = all_params.index(param)
 161          # If the number of parameters doesn't match length of type comment,
 162          # ignore first parameter (assume it's self).
 163          if len(params_comments) != len(all_params):
 164              debug.warning(
 165                  "Comments length != Params length %s %s",
 166                  params_comments, all_params
 167              )
 168          if function_value.is_bound_method():
 169              if index == 0:
 170                  # Assume it's self, which is already handled
 171                  return NO_VALUES
 172              index -= 1
 173          if index >= len(params_comments):
 174              return NO_VALUES
 175  
 176          param_comment = params_comments[index]
 177          return _infer_annotation_string(
 178              function_value.get_default_param_context(),
 179              param_comment
 180          )
 181      # Annotations are like default params and resolve in the same way.
 182      context = function_value.get_default_param_context()
 183      return infer_annotation(context, annotation)
 184  
 185  
 186  def py__annotations__(funcdef):
 187      dct = {}
 188      for function_param in funcdef.get_params():
 189          param_annotation = function_param.annotation
 190          if param_annotation is not None:
 191              dct[function_param.name.value] = param_annotation
 192  
 193      return_annotation = funcdef.annotation
 194      if return_annotation:
 195          dct['return'] = return_annotation
 196      return dct
 197  
 198  
 199  def resolve_forward_references(context, all_annotations):
 200      def resolve(node):
 201          if node is None or node.type != 'string':
 202              return node
 203  
 204          node = _get_forward_reference_node(
 205              context,
 206              context.inference_state.compiled_subprocess.safe_literal_eval(
 207                  node.value,
 208              ),
 209          )
 210  
 211          if node is None:
 212              # There was a string, but it's not a valid annotation
 213              return None
 214  
 215          # The forward reference tree has an additional root node ('eval_input')
 216          # that we don't want. Extract the node we do want, that is equivalent to
 217          # the nodes returned by `py__annotations__` for a non-quoted node.
 218          node = node.children[0]
 219  
 220          return node
 221  
 222      return {name: resolve(node) for name, node in all_annotations.items()}
 223  
 224  
 225  @inference_state_method_cache()
 226  def infer_return_types(function, arguments):
 227      """
 228      Infers the type of a function's return value,
 229      according to type annotations.
 230      """
 231      context = function.get_default_param_context()
 232      all_annotations = resolve_forward_references(
 233          context,
 234          py__annotations__(function.tree_node),
 235      )
 236      annotation = all_annotations.get("return", None)
 237      if annotation is None:
 238          # If there is no Python 3-type annotation, look for an annotation
 239          # comment.
 240          node = function.tree_node
 241          comment = parser_utils.get_following_comment_same_line(node)
 242          if comment is None:
 243              return NO_VALUES
 244  
 245          match = re.match(r"^#\s*type:\s*\([^#]*\)\s*->\s*([^#]*)", comment)
 246          if not match:
 247              return NO_VALUES
 248  
 249          return _infer_annotation_string(
 250              context,
 251              match.group(1).strip()
 252          ).execute_annotation()
 253  
 254      unknown_type_vars = find_unknown_type_vars(context, annotation)
 255      annotation_values = infer_annotation(context, annotation)
 256      if not unknown_type_vars:
 257          return annotation_values.execute_annotation()
 258  
 259      type_var_dict = infer_type_vars_for_execution(function, arguments, all_annotations)
 260  
 261      return ValueSet.from_sets(
 262          ann.define_generics(type_var_dict)
 263          if isinstance(ann, (DefineGenericBaseClass, TypeVar)) else ValueSet({ann})
 264          for ann in annotation_values
 265      ).execute_annotation()
 266  
 267  
 268  def infer_type_vars_for_execution(function, arguments, annotation_dict):
 269      """
 270      Some functions use type vars that are not defined by the class, but rather
 271      only defined in the function. See for example `iter`. In those cases we
 272      want to:
 273  
 274      1. Search for undefined type vars.
 275      2. Infer type vars with the execution state we have.
 276      3. Return the union of all type vars that have been found.
 277      """
 278      context = function.get_default_param_context()
 279  
 280      annotation_variable_results = {}
 281      executed_param_names = get_executed_param_names(function, arguments)
 282      for executed_param_name in executed_param_names:
 283          try:
 284              annotation_node = annotation_dict[executed_param_name.string_name]
 285          except KeyError:
 286              continue
 287  
 288          annotation_variables = find_unknown_type_vars(context, annotation_node)
 289          if annotation_variables:
 290              # Infer unknown type var
 291              annotation_value_set = context.infer_node(annotation_node)
 292              kind = executed_param_name.get_kind()
 293              actual_value_set = executed_param_name.infer()
 294              if kind is Parameter.VAR_POSITIONAL:
 295                  actual_value_set = actual_value_set.merge_types_of_iterate()
 296              elif kind is Parameter.VAR_KEYWORD:
 297                  # TODO _dict_values is not public.
 298                  actual_value_set = actual_value_set.try_merge('_dict_values')
 299              merge_type_var_dicts(
 300                  annotation_variable_results,
 301                  annotation_value_set.infer_type_vars(actual_value_set),
 302              )
 303      return annotation_variable_results
 304  
 305  
 306  def infer_return_for_callable(arguments, param_values, result_values):
 307      all_type_vars = {}
 308      for pv in param_values:
 309          if pv.array_type == 'list':
 310              type_var_dict = _infer_type_vars_for_callable(arguments, pv.py__iter__())
 311              all_type_vars.update(type_var_dict)
 312  
 313      return ValueSet.from_sets(
 314          v.define_generics(all_type_vars)
 315          if isinstance(v, (DefineGenericBaseClass, TypeVar))
 316          else ValueSet({v})
 317          for v in result_values
 318      ).execute_annotation()
 319  
 320  
 321  def _infer_type_vars_for_callable(arguments, lazy_params):
 322      """
 323      Infers type vars for the Calllable class:
 324  
 325          def x() -> Callable[[Callable[..., _T]], _T]: ...
 326      """
 327      annotation_variable_results = {}
 328      for (_, lazy_value), lazy_callable_param in zip(arguments.unpack(), lazy_params):
 329          callable_param_values = lazy_callable_param.infer()
 330          # Infer unknown type var
 331          actual_value_set = lazy_value.infer()
 332          merge_type_var_dicts(
 333              annotation_variable_results,
 334              callable_param_values.infer_type_vars(actual_value_set),
 335          )
 336      return annotation_variable_results
 337  
 338  
 339  def merge_type_var_dicts(base_dict, new_dict):
 340      for type_var_name, values in new_dict.items():
 341          if values:
 342              try:
 343                  base_dict[type_var_name] |= values
 344              except KeyError:
 345                  base_dict[type_var_name] = values
 346  
 347  
 348  def merge_pairwise_generics(annotation_value, annotated_argument_class):
 349      """
 350      Match up the generic parameters from the given argument class to the
 351      target annotation.
 352  
 353      This walks the generic parameters immediately within the annotation and
 354      argument's type, in order to determine the concrete values of the
 355      annotation's parameters for the current case.
 356  
 357      For example, given the following code:
 358  
 359          def values(mapping: Mapping[K, V]) -> List[V]: ...
 360  
 361          for val in values({1: 'a'}):
 362              val
 363  
 364      Then this function should be given representations of `Mapping[K, V]`
 365      and `Mapping[int, str]`, so that it can determine that `K` is `int and
 366      `V` is `str`.
 367  
 368      Note that it is responsibility of the caller to traverse the MRO of the
 369      argument type as needed in order to find the type matching the
 370      annotation (in this case finding `Mapping[int, str]` as a parent of
 371      `Dict[int, str]`).
 372  
 373      Parameters
 374      ----------
 375  
 376      `annotation_value`: represents the annotation to infer the concrete
 377          parameter types of.
 378  
 379      `annotated_argument_class`: represents the annotated class of the
 380          argument being passed to the object annotated by `annotation_value`.
 381      """
 382  
 383      type_var_dict = {}
 384  
 385      if not isinstance(annotated_argument_class, DefineGenericBaseClass):
 386          return type_var_dict
 387  
 388      annotation_generics = annotation_value.get_generics()
 389      actual_generics = annotated_argument_class.get_generics()
 390  
 391      for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics):
 392          merge_type_var_dicts(
 393              type_var_dict,
 394              annotation_generics_set.infer_type_vars(actual_generic_set.execute_annotation()),
 395          )
 396  
 397      return type_var_dict
 398  
 399  
 400  def find_type_from_comment_hint_for(context, node, name):
 401      return _find_type_from_comment_hint(context, node, node.children[1], name)
 402  
 403  
 404  def find_type_from_comment_hint_with(context, node, name):
 405      if len(node.children) > 4:
 406          # In case there are multiple with_items, we do not want a type hint for
 407          # now.
 408          return []
 409      assert len(node.children[1].children) == 3, \
 410          "Can only be here when children[1] is 'foo() as f'"
 411      varlist = node.children[1].children[2]
 412      return _find_type_from_comment_hint(context, node, varlist, name)
 413  
 414  
 415  def find_type_from_comment_hint_assign(context, node, name):
 416      return _find_type_from_comment_hint(context, node, node.children[0], name)
 417  
 418  
 419  def _find_type_from_comment_hint(context, node, varlist, name):
 420      index = None
 421      if varlist.type in ("testlist_star_expr", "exprlist", "testlist"):
 422          # something like "a, b = 1, 2"
 423          index = 0
 424          for child in varlist.children:
 425              if child == name:
 426                  break
 427              if child.type == "operator":
 428                  continue
 429              index += 1
 430          else:
 431              return []
 432  
 433      comment = parser_utils.get_following_comment_same_line(node)
 434      if comment is None:
 435          return []
 436      match = re.match(r"^#\s*type:\s*([^#]*)", comment)
 437      if match is None:
 438          return []
 439      return _infer_annotation_string(
 440          context, match.group(1).strip(), index
 441      ).execute_annotation()
 442  
 443  
 444  def find_unknown_type_vars(context, node):
 445      def check_node(node):
 446          if node.type in ('atom_expr', 'power'):
 447              trailer = node.children[-1]
 448              if trailer.type == 'trailer' and trailer.children[0] == '[':
 449                  for subscript_node in _unpack_subscriptlist(trailer.children[1]):
 450                      check_node(subscript_node)
 451          else:
 452              found[:] = _filter_type_vars(context.infer_node(node), found)
 453  
 454      found = []  # We're not using a set, because the order matters.
 455      check_node(node)
 456      return found
 457  
 458  
 459  def _filter_type_vars(value_set, found=()):
 460      new_found = list(found)
 461      for type_var in value_set:
 462          if isinstance(type_var, TypeVar) and type_var not in found:
 463              new_found.append(type_var)
 464      return new_found
 465  
 466  
 467  def _unpack_subscriptlist(subscriptlist):
 468      if subscriptlist.type == 'subscriptlist':
 469          for subscript in subscriptlist.children[::2]:
 470              if subscript.type != 'subscript':
 471                  yield subscript
 472      else:
 473          if subscriptlist.type != 'subscript':
 474              yield subscriptlist
 475