def build_class(self, class_name, bases, keywords, decorators, defs) -> pytd.Class: """Build a pytd.Class from definitions collected from an ast node.""" parents, namedtuple_index = classdef.get_parents(bases) metaclass = classdef.get_metaclass(keywords, parents) constants, methods, aliases, slots, classes = _split_definitions(defs) # Make sure we don't have duplicate definitions. classdef.check_for_duplicate_defs(methods, constants, aliases) # Generate a NamedTuple proxy base class if needed if namedtuple_index is not None: namedtuple_parent = self.new_named_tuple(class_name, [(c.name, c.type) for c in constants]) parents[namedtuple_index] = namedtuple_parent constants = [] if aliases: vals_dict = { val.name: val for val in constants + aliases + methods + classes } for val in aliases: name = val.name seen_names = set() while isinstance(val, pytd.Alias): if isinstance(val.type, pytd.NamedType): _, _, base_name = val.type.name.rpartition(".") if base_name in seen_names: # This happens in cases like: # class X: # Y = something.Y # Since we try to resolve aliases immediately, we don't know what # type to fill in when the alias value comes from outside the # class. The best we can do is Any. val = pytd.Constant(name, pytd.AnythingType()) continue seen_names.add(base_name) if base_name in vals_dict: val = vals_dict[base_name] continue # The alias value comes from outside the class. The best we can do is # to fill in Any. val = pytd.Constant(name, pytd.AnythingType()) if isinstance(val, function.NameAndSig): val = dataclasses.replace(val, name=name) methods.append(val) else: if isinstance(val, pytd.Class): t = pytdgen.pytd_type( pytd.NamedType(class_name + "." + val.name)) else: t = val.type constants.append(pytd.Constant(name, t)) parents = [p for p in parents if not isinstance(p, pytd.NothingType)] methods = function.merge_method_signatures(methods) if not parents and class_name not in ["classobj", "object"]: # A parent-less class inherits from classobj in Python 2 and from object # in Python 3. typeshed assumes the Python 3 behavior for all stubs, so we # do the same here. parents = (pytd.NamedType("object"), ) return pytd.Class(name=class_name, metaclass=metaclass, parents=tuple(parents), methods=tuple(methods), constants=tuple(constants), classes=tuple(classes), decorators=tuple(decorators), slots=slots, template=())
def _maybe_resolve_alias(alias, name_to_class, name_to_constant): """Resolve the alias if possible. Args: alias: A pytd.Alias name_to_class: A class map used for resolution. name_to_constant: A constant map used for resolution. Returns: None, if the alias pointed to an un-aliasable type. The resolved value, if the alias was resolved. The alias, if it was not resolved. """ if not isinstance(alias.type, pytd.NamedType): return alias if alias.type.name in _TYPING_SETS: # Filter out aliases to `typing` members that don't appear in typing.pytd # to avoid lookup errors. return None if "." not in alias.type.name: # We'll handle nested classes specially, since they need to be represented # as constants to distinguish them from imports. return alias parts = alias.type.name.split(".") if parts[0] not in name_to_class and parts[0] not in name_to_constant: return alias prev_value = None value = name_to_class.get(parts[0]) or name_to_constant[parts[0]] for part in parts[1:]: prev_value = value # We can immediately return upon encountering an error, as load_pytd will # complain when it can't resolve the alias. if isinstance(value, pytd.Constant): if (not isinstance(value.type, pytd.NamedType) or value.type.name not in name_to_class): # TODO(rechen): Parameterized constants of generic classes should # probably also be allowed. return alias value = name_to_class[value.type.name] if not isinstance(value, pytd.Class): return alias try: value = value.Lookup(part) except KeyError: return alias if isinstance(value, pytd.Class): return pytd.Constant( alias.name, pytdgen.pytd_type(pytd.NamedType(alias.type.name))) elif isinstance(value, pytd.Function): # We allow module-level aliases of methods from classes and class instances. # When a static method is aliased, or a normal method is aliased from a # class (not an instance), the entire method signature is copied. Otherwise, # the first parameter ('self' or 'cls') is dropped. new_value = value.Replace(name=alias.name).Replace(kind="method") if value.kind == "staticmethod" or ( value.kind == "method" and isinstance(prev_value, pytd.Class)): return new_value return new_value.Replace(signatures=tuple( s.Replace(params=s.params[1:]) for s in new_value.signatures)) else: return value.Replace(name=alias.name)
def _maybe_resolve_alias(alias, name_to_class, name_to_constant): """Resolve the alias if possible. Args: alias: A pytd.Alias name_to_class: A class map used for resolution. name_to_constant: A constant map used for resolution. Returns: None, if the alias pointed to an un-aliasable type. The resolved value, if the alias was resolved. The alias, if it was not resolved. """ if not isinstance(alias.type, pytd.NamedType): return alias if alias.type.name in _TYPING_SETS: # Filter out aliases to `typing` members that don't appear in typing.pytd # to avoid lookup errors. return None if "." not in alias.type.name: # We'll handle nested classes specially, since they need to be represented # as constants to distinguish them from imports. return alias parts = alias.type.name.split(".") if parts[0] not in name_to_class and parts[0] not in name_to_constant: return alias prev_value = None value = name_to_class.get(parts[0]) or name_to_constant[parts[0]] for part in parts[1:]: prev_value = value # We can immediately return upon encountering an error, as load_pytd will # complain when it can't resolve the alias. if isinstance(value, pytd.Constant): if (not isinstance(value.type, pytd.NamedType) or value.type.name not in name_to_class): # TODO(rechen): Parameterized constants of generic classes should # probably also be allowed. return alias value = name_to_class[value.type.name] if not isinstance(value, pytd.Class): return alias try: value = value.Lookup(part) except KeyError: for parent in value.parents: if parent.name not in name_to_class: # If the parent is unknown, we don't know whether it contains 'part', # so it cannot be resolved. return alias try: value = name_to_class[parent.name].Lookup(part) except KeyError: continue # continue up the MRO else: break # 'part' found! else: return alias # unresolved if isinstance(value, pytd.Class): return pytd.Constant( alias.name, pytdgen.pytd_type(pytd.NamedType(alias.type.name))) elif isinstance(value, pytd.Function): return pytd_utils.AliasMethod( value.Replace(name=alias.name), from_constant=isinstance(prev_value, pytd.Constant)) else: return value.Replace(name=alias.name)