Exemple #1
0
    def VisitClass(self, cls):
        """Modify the methods of a class.

    This
      (a) removes all reverse operators from the class
      (b) adds the unreversed version of reverse operators from other classes.

    Args:
      cls: An instance of pytd.Class.
    Returns:
      A new class, with modified operators.
    """
        methods_to_add = self.methods_to_add[cls]
        new_methods = []
        method_names = (set(method.name for method in cls.methods)
                        | set(methods_to_add.keys()))
        for method_name in sorted(method_names):
            if method_name in self._reverse_operator_names:
                # This is a reverse operator (__radd__ etc.). We're going to add
                # the counterpiece (__add__), so we can throw away the original.
                continue
            try:
                method = cls.Lookup(method_name)
            except KeyError:
                method = pytd.Function(method_name, ())
            # wrap the extra signatures into a method, for easier matching
            extra_signatures = methods_to_add.get(method_name, [])
            new_signatures = []
            if method_name in self._reversible_operator_names:
                # If this is a normal "unreversed" operator (__add__ etc.), see whether
                # one of signatures we got from the reversed operators takes precedence.
                for sig1 in method.signatures:
                    if not any(
                            self._MatchSignature(sig1, sig2)
                            for sig2 in extra_signatures):
                        new_signatures.append(sig1)
            else:
                new_signatures.extend(method.signatures)
            new_methods.append(
                method.Replace(signatures=(tuple(new_signatures +
                                                 extra_signatures))))
        return cls.Replace(methods=tuple(new_methods))
Exemple #2
0
def DummyMethod(name, *params):
  """Create a simple method using only "Any"s as types.

  Arguments:
    name: The name of the method
    *params: The parameter names.
  Returns:
    A pytd.Function.
  """
  def make_param(param):
    return pytd.Parameter(param, type=pytd.AnythingType(), kwonly=False,
                          optional=False, mutated_type=None)
  sig = pytd.Signature(tuple(make_param(param) for param in params),
                       starargs=None, starstarargs=None,
                       return_type=pytd.AnythingType(),
                       exceptions=(), template=())
  return pytd.Function(name=name,
                       signatures=(sig,),
                       kind=pytd.METHOD,
                       flags=0)
Exemple #3
0
    def value_to_pytd_def(self, node, v, name):
        """Get a PyTD definition for this object.

    Args:
      node: The node.
      v: The object.
      name: The object name.

    Returns:
      A PyTD definition.
    """
        if (isinstance(v, abstract.PyTDFunction)
                and not isinstance(v, typing.TypeVar)):
            return pytd.Function(
                name=name,
                signatures=tuple(sig.pytd_sig for sig in v.signatures),
                kind=v.kind,
                flags=pytd.Function.abstract_flag(v.is_abstract))
        elif isinstance(v, abstract.InterpreterFunction):
            return self._function_to_def(node, v, name)
        elif isinstance(v, abstract.ParameterizedClass):
            return pytd.Alias(name, v.get_instance_type(node))
        elif isinstance(v, abstract.PyTDClass) and v.module:
            # This happens if a module does e.g. "from x import y as z", i.e., copies
            # something from another module to the local namespace. We *could*
            # reproduce the entire class, but we choose a more dense representation.
            return v.to_type(node)
        elif isinstance(v, abstract.PyTDClass):  # a namedtuple instance
            assert name != v.name
            return pytd.Alias(name, pytd.NamedType(v.name))
        elif isinstance(v, abstract.InterpreterClass):
            if v.official_name is None or name == v.official_name:
                return self._class_to_def(node, v, name)
            else:
                return pytd.Alias(name, pytd.NamedType(v.official_name))
        elif isinstance(v, abstract.TypeParameter):
            return self._typeparam_to_def(node, v, name)
        elif isinstance(v, abstract.Unsolvable):
            return pytd.Constant(name, v.to_type(node))
        else:
            raise NotImplementedError(v.__class__.__name__)
Exemple #4
0
def merge_method_signatures(
        name_and_sigs: List[NameAndSig],
        check_unhandled_decorator: bool = True) -> List[pytd.Function]:
    """Group the signatures by name, turning each group into a function."""
    functions = collections.OrderedDict()
    for fn in name_and_sigs:
        if fn.name not in functions:
            functions[fn.name] = _DecoratedFunction.make(fn)
        else:
            functions[fn.name].add_overload(fn)
    methods = []
    for name, fn in functions.items():
        if name == "__new__" or fn.decorator == "staticmethod":
            kind = pytd.MethodTypes.STATICMETHOD
        elif name == "__init_subclass__" or fn.decorator == "classmethod":
            kind = pytd.MethodTypes.CLASSMETHOD
        elif fn.properties:
            kind = pytd.MethodTypes.PROPERTY
            # If we have only setters and/or deleters, replace them with a single
            # method foo(...) -> Any, so that we infer a constant `foo: Any` even if
            # the original method signatures are all `foo(...) -> None`. (If we have a
            # getter we use its return type, but in the absence of a getter we want to
            # fall back on Any since we cannot say anything about what the setter sets
            # the type of foo to.)
            if fn.properties.getter:
                fn.sigs = [fn.properties.getter]
            else:
                sig = fn.properties.setter or fn.properties.deleter
                fn.sigs = [sig.Replace(return_type=pytd.AnythingType())]
        elif fn.decorator and check_unhandled_decorator:
            raise ValueError("Unhandled decorator: %s" % fn.decorator)
        else:
            # Other decorators do not affect the kind
            kind = pytd.MethodTypes.METHOD
        flags = 0
        if fn.is_abstract:
            flags |= pytd.MethodFlags.ABSTRACT
        if fn.is_coroutine:
            flags |= pytd.MethodFlags.COROUTINE
        methods.append(pytd.Function(name, tuple(fn.sigs), kind, flags))
    return methods
Exemple #5
0
 def _call_traces_to_function(call_traces, prefix=""):
   funcs = collections.defaultdict(pytd_utils.OrderedSet)
   for funcvar, args, kws, retvar in call_traces:
     if isinstance(funcvar.data, abstract.BoundFunction):
       func = funcvar.data.underlying.signatures[0]
     else:
       func = funcvar.data.signatures[0]
     arg_names = func.get_parameter_names()
     arg_types = (a.data.to_type()
                  for a in func.get_bound_arguments() + list(args))
     ret = pytd_utils.JoinTypes(t.to_type() for t in retvar.data)
     funcs[funcvar.data.name].add(pytd.Signature(
         tuple(pytd.Parameter(n, t)
               for n, t in zip(arg_names, arg_types)) +
         tuple(pytd.Parameter(name, a.data.to_type())
               for name, a in kws),
         ret, has_optional=False, exceptions=(), template=()))
   functions = []
   for name, signatures in funcs.items():
     functions.append(pytd.Function(prefix + name, tuple(signatures)))
   return functions
Exemple #6
0
def _merge_method_signatures(signatures):
  """Group the signatures by name, turning each group into a function."""
  name_to_signatures = collections.OrderedDict()
  name_to_decorator = {}
  name_to_is_abstract = collections.defaultdict(bool)
  # map from function name to a bool indicating whether the function has an
  # external definition
  name_to_external_code = {}
  for name, signature, decorator, external_code, is_abstract in signatures:
    if name not in name_to_signatures:
      name_to_signatures[name] = []
      name_to_decorator[name] = decorator
    if name_to_decorator[name] != decorator:
      raise ParseError(
          "Overloaded signatures for %s disagree on decorators" % name)
    if name in name_to_external_code:
      if external_code and name_to_external_code[name]:
        raise ParseError("Multiple PYTHONCODEs for %s" % name)
      elif external_code != name_to_external_code[name]:
        raise ParseError("Mixed pytd and PYTHONCODEs for %s" % name)
    else:
      name_to_external_code[name] = external_code
    name_to_signatures[name].append(signature)
    name_to_is_abstract[name] |= is_abstract
  methods = []
  for name, signatures in name_to_signatures.items():
    decorator = name_to_decorator[name]
    is_abstract = name_to_is_abstract[name]
    if name == "__new__" or decorator == "staticmethod":
      kind = pytd.STATICMETHOD
    elif decorator == "classmethod":
      kind = pytd.CLASSMETHOD
    else:
      kind = pytd.METHOD
    if name_to_external_code[name]:
      methods.append(pytd.ExternalFunction(name, (), kind, is_abstract))
    else:
      methods.append(pytd.Function(name, tuple(signatures), kind, is_abstract))
  return methods
Exemple #7
0
    def testComplexCombinedType(self):
        """Test parsing a type with both union and intersection."""

        data1 = r"def foo(a: Foo or Bar and Zot) -> object"
        data2 = r"def foo(a: Foo or (Bar and Zot)) -> object"
        result1 = self.Parse(data1)
        result2 = self.Parse(data2)
        f = pytd.Function(
            name="foo",
            signatures=(pytd.Signature(params=(pytd.Parameter(
                name="a",
                type=pytd.UnionType(
                    type_list=(pytd.NamedType("Foo"),
                               pytd.IntersectionType(
                                   type_list=(pytd.NamedType("Bar"),
                                              pytd.NamedType("Zot")))))), ),
                                       return_type=pytd.NamedType("object"),
                                       template=(),
                                       has_optional=False,
                                       exceptions=()), ))
        self.assertEqual(f, result1.Lookup("foo"))
        self.assertEqual(f, result2.Lookup("foo"))
Exemple #8
0
 def _simple_func_to_def(self, node, v, name):
     """Convert a SimpleFunction to a PyTD definition."""
     sig = v.signature
     params = [
         pytd.Parameter(p, sig.annotations[p].get_instance_type(node),
                        False, p in sig.defaults, None)
         for p in sig.param_names
     ]
     kwonly = [
         pytd.Parameter(p, sig.annotations[p].get_instance_type(node), True,
                        p in sig.defaults, None) for p in sig.kwonly_params
     ]
     if sig.varargs_name:
         star = pytd.Parameter(
             sig.varargs_name,
             sig.annotations[sig.varargs_name].get_instance_type(node),
             False, False, None)
     else:
         star = None
     if sig.kwargs_name:
         starstar = pytd.Parameter(
             sig.kwargs_name,
             sig.annotations[sig.kwargs_name].get_instance_type(node),
             False, False, None)
     else:
         starstar = None
     if sig.has_return_annotation:
         ret_type = sig.annotations["return"].get_instance_type(node)
     else:
         ret_type = pytd.NamedType("__builtin__.NoneType")
     pytd_sig = pytd.Signature(params=tuple(params + kwonly),
                               starargs=star,
                               starstarargs=starstar,
                               return_type=ret_type,
                               exceptions=(),
                               template=())
     return pytd.Function(name, (pytd_sig, ), pytd.METHOD)
Exemple #9
0
 def _call_traces_to_function(call_traces, name_transform=lambda x: x):
   funcs = collections.defaultdict(pytd_utils.OrderedSet)
   for node, func, sigs, args, kws, retvar in call_traces:
     # The lengths may be different in the presence of optional and kw args.
     arg_names = max((sig.get_positional_names() for sig in sigs), key=len)
     for i in range(len(arg_names)):
       if not isinstance(func.data, abstract.BoundFunction) or i > 0:
         arg_names[i] = function.argname(i)
     arg_types = (a.data.to_type(node) for a in args)
     ret = pytd_utils.JoinTypes(t.to_type(node) for t in retvar.data)
     starargs = None
     starstarargs = None
     funcs[func.data.name].add(pytd.Signature(
         tuple(pytd.Parameter(n, t, False, False, None)
               for n, t in zip(arg_names, arg_types)) +
         tuple(pytd.Parameter(name, a.data.to_type(node), False, False, None)
               for name, a in kws),
         starargs, starstarargs,
         ret, exceptions=(), template=()))
   functions = []
   for name, signatures in funcs.items():
     functions.append(pytd.Function(name_transform(name), tuple(signatures),
                                    pytd.MethodTypes.METHOD))
   return functions
Exemple #10
0
def WrapTypeDeclUnit(name, items):
    """Given a list (classes, functions, etc.), wrap a pytd around them.

  Args:
    name: The name attribute of the resulting TypeDeclUnit.
    items: A list of items. Can contain pytd.Class, pytd.Function and
      pytd.Constant.
  Returns:
    A pytd.TypeDeclUnit.
  Raises:
    ValueError: In case of an invalid item in the list.
    NameError: For name conflicts.
  """

    functions = collections.OrderedDict()
    classes = collections.OrderedDict()
    constants = collections.defaultdict(TypeBuilder)
    aliases = collections.OrderedDict()
    typevars = collections.OrderedDict()
    for item in items:
        if isinstance(item, pytd.Function):
            if item.name in functions:
                if item.kind != functions[item.name].kind:
                    raise ValueError("Can't combine %s and %s" %
                                     (item.kind, functions[item.name].kind))
                functions[item.name] = pytd.Function(
                    item.name,
                    functions[item.name].signatures + item.signatures,
                    item.kind)
            else:
                functions[item.name] = item
        elif isinstance(item, pytd.Class):
            if item.name in classes:
                raise NameError("Duplicate top level class: %r" % item.name)
            classes[item.name] = item
        elif isinstance(item, pytd.Constant):
            constants[item.name].add_type(item.type)
        elif isinstance(item, pytd.Alias):
            if item.name in aliases:
                raise NameError("Duplicate top level alias or import: %r" %
                                item.name)
            aliases[item.name] = item
        elif isinstance(item, pytd.TypeParameter):
            if item.name in typevars:
                raise NameError("Duplicate top level type parameter: %r" %
                                item.name)
            typevars[item.name] = item
        else:
            raise ValueError("Invalid top level pytd item: %r" % type(item))

    categories = {
        "function": functions,
        "class": classes,
        "constant": constants,
        "alias": aliases,
        "typevar": typevars
    }
    for c1, c2 in itertools.combinations(categories, 2):
        _check_intersection(categories[c1], categories[c2], c1, c2)

    return pytd.TypeDeclUnit(name=name,
                             constants=tuple(
                                 pytd.Constant(name, t.build())
                                 for name, t in sorted(constants.items())),
                             type_params=tuple(typevars.values()),
                             classes=tuple(classes.values()),
                             functions=tuple(functions.values()),
                             aliases=tuple(aliases.values()))
Exemple #11
0
def _merge_signatures(signatures):
    """Given a list of pytd function signature declarations, group them by name.

  Converts a list of NameAndSig items to a list of Functions and a list of
  Constants (grouping signatures by name). Constants are derived from
  functions with @property decorators.

  Arguments:
    signatures: List[NameAndSig].

  Returns:
    Tuple[List[pytd.Function], List[pytd.Constant]].

  Raises:
    ParseError: if an error is encountered while trying to merge signatures.
  """
    name_to_property_type = collections.OrderedDict()
    method_signatures = []
    for signature in signatures:
        is_property, property_type = _try_parse_signature_as_property(
            signature)
        if is_property:
            # Any methods with a decorator that looks like one of {@property,
            # @foo.setter, @foo.deleter) will get merged into a Constant.
            name = signature.name
            if property_type or name not in name_to_property_type:
                name_to_property_type[name] = property_type
                # TODO(acaceres): warn if incompatible types? Or only take type
                # from @property, not @foo.setter? Take last non-None for now.
        else:
            method_signatures.append(signature)

    name_to_signatures = collections.OrderedDict()
    # map name to (# external_code is {False,True}):
    name_external = collections.defaultdict(lambda: {False: 0, True: 0})

    name_to_decorators = {}
    for name, signature, decorators, external_code in method_signatures:
        if name in name_to_property_type:
            raise ParseError("Incompatible signatures for %s" % name)

        if name not in name_to_signatures:
            name_to_signatures[name] = []
            name_to_decorators[name] = decorators

        if name_to_decorators[name] != decorators:
            raise ParseError(
                "Overloaded signatures for %s disagree on decorators" % name)

        name_to_signatures[name].append(signature)
        name_external[name][external_code] += 1

    _verify_python_code(name_external)
    methods = []
    for name, signatures in name_to_signatures.items():
        kind = pytd.METHOD
        decorators = name_to_decorators[name]
        if "classmethod" in decorators:
            kind = pytd.CLASSMETHOD
        if name == "__new__" or "staticmethod" in decorators:
            kind = pytd.STATICMETHOD
        if name_external[name][True]:
            methods.append(pytd.ExternalFunction(name, (), kind))
        else:
            methods.append(pytd.Function(name, tuple(signatures), kind))

    constants = []
    for name, property_type in name_to_property_type.items():
        if not property_type:
            property_type = pytd.AnythingType()
        constants.append(pytd.Constant(name, property_type))

    return methods, constants
Exemple #12
0
    def _class_to_def(self, node, v, class_name):
        """Convert an InterpreterClass to a PyTD definition."""
        self._scopes.append(class_name)
        methods = {}
        constants = collections.defaultdict(pytd_utils.TypeBuilder)

        annots = abstract_utils.get_annotations_dict(v.members)
        annotated_names = set()

        def add_constants(iterator):
            for name, t in iterator:
                if t is None:
                    # Remove the entry from constants
                    annotated_names.add(name)
                elif name not in annotated_names:
                    constants[name].add_type(t)
                    annotated_names.add(name)

        add_constants(
            self._ordered_attrs_to_instance_types(node, v.metadata, annots))
        add_constants(self.annotations_to_instance_types(node, annots))

        def get_decorated_method(name, value, func_slot):
            fvar = getattr(value, func_slot)
            func = abstract_utils.get_atomic_value(fvar, abstract.Function)
            defn = self.value_to_pytd_def(node, func, name)
            defn = defn.Visit(visitors.DropMutableParameters())
            return defn

        def add_decorated_method(name, value, kind):
            try:
                defn = get_decorated_method(name, value, "func")
            except (AttributeError, abstract_utils.ConversionError):
                constants[name].add_type(pytd.AnythingType())
                return
            defn = defn.Replace(kind=kind)
            methods[name] = defn

        # If decorators are output as aliases to NamedTypes, they will be converted
        # to Functions and fail a verification step if those functions have type
        # parameters. Since we just want the function name, and since we have a
        # fully resolved name at this stage, we just output a minimal pytd.Function
        sig = pytd.Signature((), None, None, pytd.AnythingType(), (), ())
        decorators = [
            pytd.Alias(x, pytd.Function(x, (sig, ), pytd.MethodTypes.METHOD,
                                        0)) for x in v.decorators
        ]

        # class-level attributes
        for name, member in v.members.items():
            if name in CLASS_LEVEL_IGNORE or name in annotated_names:
                continue
            for value in member.FilteredData(self.vm.exitpoint, strict=False):
                if isinstance(value, special_builtins.PropertyInstance):
                    # For simplicity, output properties as constants, since our parser
                    # turns them into constants anyway.
                    if value.fget:
                        for typ in self._function_to_return_types(
                                node, value.fget):
                            constants[name].add_type(
                                pytd.Annotated(typ, ("'property'", )))
                    else:
                        constants[name].add_type(
                            pytd.Annotated(pytd.AnythingType(),
                                           ("'property'", )))
                elif isinstance(value, special_builtins.StaticMethodInstance):
                    add_decorated_method(name, value,
                                         pytd.MethodTypes.STATICMETHOD)
                elif isinstance(value, special_builtins.ClassMethodInstance):
                    add_decorated_method(name, value,
                                         pytd.MethodTypes.CLASSMETHOD)
                elif isinstance(value, abstract.Function):
                    # value_to_pytd_def returns different pytd node types depending on the
                    # input type, which pytype struggles to reason about.
                    method = cast(pytd.Function,
                                  self.value_to_pytd_def(node, value, name))
                    keep = lambda name: not name or name.startswith(v.name)
                    signatures = tuple(
                        s for s in method.signatures
                        if not s.params or keep(s.params[0].type.name))
                    if signatures and signatures != method.signatures:
                        # Filter out calls made from subclasses unless they are the only
                        # ones recorded; when inferring types for ParentClass.__init__, we
                        # do not want `self: Union[ParentClass, Subclass]`.
                        method = method.Replace(signatures=signatures)
                    # TODO(rechen): Removing mutations altogether won't work for generic
                    # classes. To support those, we'll need to change the mutated type's
                    # base to the current class, rename aliased type parameters, and
                    # replace any parameter not in the class or function template with
                    # its upper value.
                    methods[name] = method.Visit(
                        visitors.DropMutableParameters())
                else:
                    cls = self.vm.convert.merge_classes([value])
                    node, attr = self.vm.attribute_handler.get_attribute(
                        node, cls, "__get__")
                    if attr:
                        # This attribute is a descriptor. Its type is the return value of
                        # its __get__ method.
                        for typ in self._function_to_return_types(node, attr):
                            constants[name].add_type(typ)
                    else:
                        constants[name].add_type(value.to_type(node))

        # Instance-level attributes: all attributes from 'canonical' instances (that
        # is, ones created by analyze.py:analyze_class()) are added. Attributes from
        # non-canonical instances are added if their canonical values do not contain
        # type parameters.
        ignore = set(annotated_names)
        canonical_attributes = set()

        def add_attributes_from(instance):
            for name, member in instance.members.items():
                if name in CLASS_LEVEL_IGNORE or name in ignore:
                    continue
                for value in member.FilteredData(self.vm.exitpoint,
                                                 strict=False):
                    typ = value.to_type(node)
                    if pytd_utils.GetTypeParameters(typ):
                        # This attribute's type comes from an annotation that contains a
                        # type parameter; we do not want to merge in substituted values of
                        # the type parameter.
                        canonical_attributes.add(name)
                    constants[name].add_type(typ)

        for instance in v.canonical_instances:
            add_attributes_from(instance)
        ignore |= canonical_attributes
        for instance in v.instances - v.canonical_instances:
            add_attributes_from(instance)

        for name in list(methods):
            if name in constants:
                # If something is both a constant and a method, it means that the class
                # is, at some point, overwriting its own methods with an attribute.
                del methods[name]
                constants[name].add_type(pytd.AnythingType())

        constants = [
            pytd.Constant(name, builder.build())
            for name, builder in constants.items() if builder
        ]

        metaclass = v.metaclass(node)
        if metaclass is not None:
            metaclass = metaclass.get_instance_type(node)

        # Some of the class's bases may not be in global scope, so they won't show
        # up in the output. In that case, fold the base class's type information
        # into this class's pytd.
        bases = []
        missing_bases = []
        for basevar in v.bases():
            if basevar.data == [self.vm.convert.oldstyleclass_type]:
                continue
            elif len(basevar.bindings) == 1:
                b, = basevar.data
                if b.official_name is None and isinstance(
                        b, abstract.InterpreterClass):
                    missing_bases.append(b)
                else:
                    bases.append(b.get_instance_type(node))
            else:
                bases.append(
                    pytd_utils.JoinTypes(
                        b.get_instance_type(node) for b in basevar.data))

        # Collect nested classes
        # TODO(mdemello): We cannot put these in the output yet; they fail in
        # load_dependencies because of the dotted class name (google/pytype#150)
        classes = [
            self._class_to_def(node, x, x.name) for x in v.get_inner_classes()
        ]
        classes = [x.Replace(name=class_name + "." + x.name) for x in classes]

        cls = pytd.Class(name=class_name,
                         metaclass=metaclass,
                         parents=tuple(bases),
                         methods=tuple(methods.values()),
                         constants=tuple(constants),
                         classes=(),
                         decorators=tuple(decorators),
                         slots=v.slots,
                         template=())
        for base in missing_bases:
            base_cls = self.value_to_pytd_def(node, base, base.name)
            cls = pytd_utils.MergeBaseClass(cls, base_cls)
        self._scopes.pop()
        return cls
Exemple #13
0
 def generate_ast(self):
   return pytd.Function(
       name=self.name,
       signatures=tuple(s.pytd_sig for s in self.signatures),
       kind=self.kind,
       flags=pytd.MethodFlag.abstract_flag(self.is_abstract))