def pytd_annotated(parameters: List[Any]) -> pytd.Type: """Create a pytd.Annotated.""" if len(parameters) < 2: raise ParseError("typing.Annotated takes at least two parameters: " "Annotated[type, annotation, ...].") typ, *annotations = parameters annotations = tuple(map(_convert_annotated, annotations)) return pytd.Annotated(typ, annotations)
def VisitClass(self, node): constants = list(node.constants) for fn in self.const_properties[-1]: ptypes = [x.return_type for x in fn.signatures] prop = pytd.Annotated(base_type=pytd_utils.JoinTypes(ptypes), annotations=("'property'",)) constants.append(pytd.Constant(name=fn.name, type=prop)) methods = [x for x in node.methods if x not in self.const_properties[-1]] return node.Replace(constants=tuple(constants), methods=tuple(methods))
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.Module): return pytd.Alias(name, pytd.Module(name, module_name=v.full_name)) elif isinstance(v, abstract.BoundFunction): d = self.value_to_pytd_def(node, v.underlying, name) assert isinstance(d, pytd.Function) sigs = tuple( sig.Replace(params=sig.params[1:]) for sig in d.signatures) return d.Replace(signatures=sigs) elif isinstance(v, attr_overlay.Attrs): ret = pytd.NamedType("typing.Callable") md = metadata.to_pytd(v.to_metadata()) return pytd.Annotated(ret, ("'pytype_metadata'", md)) elif (isinstance(v, abstract.PyTDFunction) and not isinstance(v, typing_overlay.TypeVar)): return pytd.Function( name=name, signatures=tuple(sig.pytd_sig for sig in v.signatures), kind=v.kind, flags=pytd.MethodFlag.abstract_flag(v.is_abstract)) elif isinstance(v, abstract.InterpreterFunction): return self._function_to_def(node, v, name) elif isinstance(v, abstract.SimpleFunction): return self._simple_func_to_def(node, v, name) elif isinstance(v, (abstract.ParameterizedClass, abstract.Union)): 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, typed_dict.TypedDictClass): return self._typed_dict_to_def(node, v, name) 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__)
def pytd_annotated(parameters: List[Any]) -> pytd_node.Node: """Create a pytd.Annotated.""" if len(parameters) < 2: raise ParseError("typing.Annotated takes at least two parameters: " "Annotated[type, 'annotation', ...].") typ, *annotations = parameters if not all(isinstance(x, Constant) for x in annotations): raise ParseError("Annotations needs to be string literals: " "Annotated[type, 'annotation', ...].") annotations = tuple(x.repr_str() for x in annotations) return pytd.Annotated(typ, annotations)
def add_attributes_from(instance): for name, member in instance.members.items(): if name in abstract_utils.CLASS_LEVEL_IGNORE or name in ignore: continue for value in member.FilteredData(self.ctx.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) if v.is_enum: # If the containing class (v) is an enum, then output the instance # attributes as properties. # https://typing.readthedocs.io/en/latest/stubs.html#enums typ = pytd.Annotated(typ, ("'property'", )) constants[name].add_type(typ)
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() for name, t in self.annotations_to_instance_types(node, annots): constants[name].add_type(t) annotated_names.add(name) # 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): try: methods[name] = self._static_method_to_def( node, value, name, pytd.MethodTypes.STATICMETHOD) except abstract_utils.ConversionError: constants[name].add_type(pytd.AnythingType()) elif isinstance(value, special_builtins.ClassMethodInstance): try: methods[name] = self._class_method_to_def( node, value, name, pytd.MethodTypes.CLASSMETHOD) except abstract_utils.ConversionError: constants[name].add_type(pytd.AnythingType()) elif isinstance(value, abstract.Function): # 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] = self.value_to_pytd_def( node, value, name).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=(), 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
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
def build(self): """Get a union of all the types added so far.""" if self.tags: return pytd.Annotated(self.union, tuple(sorted(self.tags))) else: return self.union
def value_to_pytd_type(self, node, v, seen, view): """Get a PyTD type representing this object, as seen at a node. Args: node: The node from which we want to observe this object. v: The object. seen: The set of values seen before while computing the type. view: A Variable -> binding map. Returns: A PyTD type. """ if isinstance(v, (abstract.Empty, typing_overlay.NoReturn)): return pytd.NothingType() elif isinstance(v, abstract.TypeParameterInstance): if (v.module in self._scopes or v.instance is abstract_utils.DUMMY_CONTAINER): return self._typeparam_to_def(node, v.param, v.param.name) elif v.instance.get_instance_type_parameter(v.full_name).bindings: # The type parameter was initialized. Set the view to None, since we # don't include v.instance in the view. return pytd_utils.JoinTypes( self.value_to_pytd_type(node, p, seen, None) for p in v.instance.get_instance_type_parameter(v.full_name).data) elif v.param.constraints: return pytd_utils.JoinTypes( self.value_instance_to_pytd_type(node, p, None, seen, view) for p in v.param.constraints) elif v.param.bound: return self.value_instance_to_pytd_type( node, v.param.bound, None, seen, view) else: return pytd.AnythingType() elif isinstance(v, typing_overlay.TypeVar): return pytd.NamedType("builtins.type") elif isinstance(v, dataclass_overlay.FieldInstance): if not v.default: return pytd.AnythingType() return pytd_utils.JoinTypes( self.value_to_pytd_type(node, d, seen, view) for d in v.default.data) elif isinstance(v, attr_overlay.AttribInstance): ret = self.value_to_pytd_type(node, v.typ, seen, view) md = metadata.to_pytd(v.to_metadata()) return pytd.Annotated(ret, ("'pytype_metadata'", md)) elif isinstance(v, special_builtins.PropertyInstance): return pytd.NamedType("builtins.property") elif isinstance(v, typed_dict.TypedDict): return pytd.NamedType(v.props.name) elif isinstance(v, abstract.FUNCTION_TYPES): try: signatures = function.get_signatures(v) except NotImplementedError: return pytd.NamedType("typing.Callable") if len(signatures) == 1: val = self.signature_to_callable(signatures[0]) if not isinstance( v, abstract.PYTD_FUNCTION_TYPES) or not val.formal: # This is a workaround to make sure we don't put unexpected type # parameters in call traces. return self.value_instance_to_pytd_type( node, val, None, seen, view) return pytd.NamedType("typing.Callable") elif isinstance(v, (abstract.ClassMethod, abstract.StaticMethod)): return self.value_to_pytd_type(node, v.method, seen, view) elif isinstance(v, (special_builtins.IsInstance, special_builtins.ClassMethodCallable)): return pytd.NamedType("typing.Callable") elif isinstance(v, abstract.Class): param = self.value_instance_to_pytd_type(node, v, None, seen, view) return pytd.GenericType(base_type=pytd.NamedType("builtins.type"), parameters=(param, )) elif isinstance(v, abstract.Module): return pytd.Alias(v.name, pytd.Module(v.name, module_name=v.full_name)) elif (self._output_mode >= Converter.OutputMode.LITERAL and isinstance(v, abstract.ConcreteValue) and isinstance(v.pyval, (int, str, bytes))): # LITERAL mode is used only for pretty-printing, so we just stringify the # inner value rather than properly converting it. return pytd.Literal(repr(v.pyval)) elif isinstance(v, abstract.SimpleValue): ret = self.value_instance_to_pytd_type(node, v.cls, v, seen=seen, view=view) ret.Visit( visitors.FillInLocalPointers( {"builtins": self.ctx.loader.builtins})) return ret elif isinstance(v, abstract.Union): return pytd_utils.JoinTypes( self.value_to_pytd_type(node, o, seen, view) for o in v.options) elif isinstance(v, special_builtins.SuperInstance): return pytd.NamedType("builtins.super") elif isinstance(v, abstract.TypeParameter): # Arguably, the type of a type parameter is NamedType("typing.TypeVar"), # but pytype doesn't know how to handle that, so let's just go with Any # unless self._detailed is set. if self._detailed: return pytd.NamedType("typing.TypeVar") else: return pytd.AnythingType() elif isinstance(v, abstract.Unsolvable): return pytd.AnythingType() elif isinstance(v, abstract.Unknown): return pytd.NamedType(v.class_name) elif isinstance(v, abstract.BuildClass): return pytd.NamedType("typing.Callable") elif isinstance(v, abstract.FinalAnnotation): param = self.value_to_pytd_type(node, v.annotation, seen, view) return pytd.GenericType(base_type=pytd.NamedType("typing.Final"), parameters=(param, )) else: raise NotImplementedError(v.__class__.__name__)