def testStrict(self): ast = parser.parse_string( textwrap.dedent(""" T = TypeVar('T') class list(typing.Generic[T], object): pass class A(): pass class B(A): pass class `~unknown0`(): pass a = ... # type: A def left() -> `~unknown0` def right() -> list[A] """)) ast = visitors.LookupClasses(ast, self.mini_builtins) m = type_match.TypeMatch(type_match.get_all_subclasses([ast])) left, right = ast.Lookup("left"), ast.Lookup("right") self.assertEquals( m.match(left, right, {}), booleq.And((booleq.Eq("~unknown0", "list"), booleq.Eq("~unknown0.list.T", "A"))))
def test_strict(self): ast = parser.parse_string(textwrap.dedent(""" import typing T = TypeVar('T') class list(typing.Generic[T], object): pass class A(): pass class B(A): pass class `~unknown0`(): pass a = ... # type: A def left() -> `~unknown0` def right() -> list[A] """), python_version=self.python_version) ast = self.LinkAgainstSimpleBuiltins(ast) m = type_match.TypeMatch(type_match.get_all_subclasses([ast])) left, right = ast.Lookup("left"), ast.Lookup("right") self.assertEqual( m.match(left, right, {}), booleq.And((booleq.Eq("~unknown0", "list"), booleq.Eq("~unknown0.list.T", "A"))))
def _TestTypeParameters(self, reverse=False): ast = parser.parse_string(pytd_src(""" from typing import Any, Generic class `~unknown0`(): def next(self) -> Any: ... T = TypeVar('T') class A(Generic[T], object): def next(self) -> Any: ... class B(): pass def left(x: `~unknown0`) -> Any: ... def right(x: A[B]) -> Any: ... """), options=self.options) ast = self.LinkAgainstSimpleBuiltins(ast) m = type_match.TypeMatch() left, right = ast.Lookup("left"), ast.Lookup("right") match = m.match(right, left, {}) if reverse else m.match( left, right, {}) unknown0 = escape.unknown(0) self.assertEqual( match, booleq.And((booleq.Eq(unknown0, "A"), booleq.Eq(f"{unknown0}.A.T", "B")))) self.assertIn(f"{unknown0}.A.T", m.solver.variables)
def match_Generic_against_Generic(self, t1, t2, subst): # pylint: disable=invalid-name """Match a pytd.GenericType against another pytd.GenericType.""" assert isinstance(t1.base_type, pytd.ClassType), type(t1.base_type) assert isinstance(t2.base_type, pytd.ClassType), type(t2.base_type) base1 = pytd.ClassType(t1.base_type.cls.name, t1.base_type.cls) base2 = pytd.ClassType(t2.base_type.cls.name, t2.base_type.cls) base_type_cmp = self.match_type_against_type(base1, base2, subst) if base_type_cmp is booleq.FALSE: return booleq.FALSE if not isinstance(t1, pytd.TupleType) and isinstance( t2, pytd.TupleType): p1, = t1.parameters param_cmp = [ self.match_type_against_type(p1, p2, subst) for p2 in t2.parameters ] else: t1_parameters = t1.parameters if isinstance(t1, pytd.TupleType): if isinstance(t2, pytd.TupleType): if len(t1_parameters) != len(t2.parameters): return booleq.FALSE else: t1_parameters = (pytd.UnionType(type_list=t1_parameters), ) # Matching, e.g., Dict[str, int] against Iterable[K] is legitimate. assert len(t1_parameters) >= len( t2.parameters), t1.base_type.cls.name # Type parameters are covariant: # E.g. passing list[int] as argument for list[object] succeeds. param_cmp = [ self.match_type_against_type(p1, p2, subst) for p1, p2 in zip(t1_parameters, t2.parameters) ] return booleq.And([base_type_cmp] + param_cmp)
def test_strict(self): ast = parser.parse_string(pytd_src(""" import typing T = TypeVar('T') class list(typing.Generic[T], object): pass class A(): pass class B(A): pass class `~unknown0`(): pass a = ... # type: A def left() -> `~unknown0`: ... def right() -> list[A]: ... """), options=self.options) ast = self.LinkAgainstSimpleBuiltins(ast) m = type_match.TypeMatch(type_match.get_all_subclasses([ast])) left, right = ast.Lookup("left"), ast.Lookup("right") unknown0 = escape.unknown(0) self.assertEqual( m.match(left, right, {}), booleq.And((booleq.Eq(unknown0, "list"), booleq.Eq(f"{unknown0}.list.T", "A"))))
def match_Signature_against_Function(self, sig, f, subst, skip_self=False): # pylint: disable=invalid-name def make_or(inner): return booleq.Or( self.match_Signature_against_Signature( inner, s, subst, skip_self) for s in f.signatures) return booleq.And( make_or(inner) for inner in visitors.ExpandSignature(sig))
def match_Signature_against_Signature(self, sig1, sig2, subst, skip_self=False): """Match a pytd.Signature against another pytd.Signature. Args: sig1: The caller sig2: The callee subst: Current type parameters. skip_self: If True, doesn't compare the first parameter, which is considered (and verified) to be "self". Returns: An instance of booleq.BooleanTerm, i.e. a boolean formula. """ assert not sig1.template # Signatures have type parameters, too. We ignore them, since they can # be anything. (See maybe_lookup_type_param()) subst.update({p.type_param: None for p in sig2.template}) params1 = sig1.params params2 = sig2.params if skip_self: # Methods in an ~unknown need to declare their methods with "self" assert params1 and params1[0].name == "self" params1 = params1[1:] if params2 and params2[0].name == "self": params2 = params2[1:] equalities = [] if len(params1) > len(params2) and not sig2.has_optional: return booleq.FALSE # extra parameters if sig1.starargs is not None and sig2.starargs is not None: equalities.append(self.match_type_against_type( sig1.starargs.type, sig2.starargs.type, subst)) if sig1.starstarargs is not None and sig2.starstarargs is not None: equalities.append(self.match_type_against_type( sig1.starstarargs.type, sig2.starstarargs.type, subst)) # TODO(kramm): Handle kwonly parameters (on either side). Presumably, a # kwonly on the left side means that it was a keyword param. for p1, p2 in zip(params1, params2): if p1.optional and not p2.optional: return booleq.FALSE # needed for optimize.py:RemoveRedundantSignatures for i, p2 in enumerate(params2): if i >= len(params1): if not p2.optional: return booleq.FALSE # missing parameter else: pass else: p1 = params1[i] if p1.name != p2.name and not ( pytd_utils.ANON_PARAM.match(p1.name) or pytd_utils.ANON_PARAM.match(p2.name)): return booleq.FALSE equalities.append(self.match_type_against_type(p1.type, p2.type, subst)) equalities.append( self.match_type_against_type( sig1.return_type, sig2.return_type, subst)) return booleq.And(equalities)
def match_Functions_against_Class(self, methods, cls2, subst): implications = [] cache = {} for f1 in methods: implication = self.match_Function_against_Class(f1, cls2, subst, cache) implications.append(implication) if implication is booleq.FALSE: break # TODO(b/159058933): class attributes return booleq.And(implications)
def match_Class_against_Class(self, cls1, cls2, subst): # pylint: disable=invalid-name """Match a pytd.Class against another pytd.Class.""" implications = [] cache = {} for f1 in cls1.methods: implication = self.match_Function_against_Class( f1, cls2, subst, cache) implications.append(implication) if implication is booleq.FALSE: break # TODO(kramm): class attributes return booleq.And(implications)
def match_Unknown_against_Generic(self, t1, t2, subst): # pylint: disable=invalid-name assert isinstance(t2.base_type, pytd.ClassType) # No inheritance for base classes - you can only inherit from an # instantiated template, but not from a template itself. base_match = booleq.Eq(t1.name, t2.base_type.cls.name) type_params = [ self.type_parameter(t1, t2.base_type.cls, item) for item in t2.base_type.cls.template ] for type_param in type_params: self.solver.register_variable(type_param.name) params = [ self.match_type_against_type(p1, p2, subst) for p1, p2 in zip(type_params, t2.parameters) ] return booleq.And([base_match] + params)
def match_Generic_against_Generic(self, t1, t2, subst): # pylint: disable=invalid-name """Match a pytd.GenericType against another pytd.GenericType.""" assert isinstance(t1.base_type, pytd.ClassType), type(t1.base_type) assert isinstance(t2.base_type, pytd.ClassType), type(t2.base_type) base1 = pytd.ClassType(t1.base_type.cls.name, t1.base_type.cls) base2 = pytd.ClassType(t2.base_type.cls.name, t2.base_type.cls) base_type_cmp = self.match_type_against_type(base1, base2, subst) if base_type_cmp is booleq.FALSE: return booleq.FALSE assert len(t1.parameters) == len(t2.parameters), t1.base_type.cls.name # Type parameters are covariant: # E.g. passing list[int] as argument for list[object] succeeds. param_cmp = [ self.match_type_against_type(p1, p2, subst) for p1, p2 in zip(t1.parameters, t2.parameters) ] return booleq.And([base_type_cmp] + param_cmp)
def match_Signature_against_Signature(self, sig1, sig2, subst, skip_self=False): """Match a pytd.Signature against another pytd.Signature. Args: sig1: The caller sig2: The callee subst: Current type parameters. skip_self: If True, doesn't compare the first paramter, which is considered (and verified) to be "self". Returns: An instance of booleq.BooleanTerm, i.e. a boolean formula. """ assert not sig1.template # Signatures have type parameters, too. We ignore them, since they can # be anything. (See maybe_lookup_type_param()) subst.update({p.type_param: None for p in sig2.template}) params1 = sig1.params params2 = sig2.params if skip_self: # Methods in an ~unknown need to declare their methods with "self" assert params1 and params1[0].name == "self" params1 = params1[1:] if params2 and params2[0].name == "self": params2 = params2[1:] equalities = [] if len(params1) > len(params2) and not sig2.has_optional: return booleq.FALSE # extra parameters for i, p2 in enumerate(params2): if not p2.optional: if i >= len(params1): return booleq.FALSE # missing parameter p1 = params1[i] equalities.append( self.match_type_against_type(p1.type, p2.type, subst)) equalities.append( self.match_type_against_type(sig1.return_type, sig2.return_type, subst)) return booleq.And(equalities)
def _TestTypeParameters(self, reverse=False): ast = parser.parse_string(textwrap.dedent(""" import typing class `~unknown0`(): def next(self) -> ? T = TypeVar('T') class A(typing.Generic[T], object): def next(self) -> ? class B(): pass def left(x: `~unknown0`) -> ? def right(x: A[B]) -> ? """), python_version=self.PYTHON_VERSION) ast = self.LinkAgainstSimpleBuiltins(ast) m = type_match.TypeMatch() left, right = ast.Lookup("left"), ast.Lookup("right") match = m.match(right, left, {}) if reverse else m.match(left, right, {}) self.assertEqual(match, booleq.And((booleq.Eq("~unknown0", "A"), booleq.Eq("~unknown0.A.T", "B")))) self.assertIn("~unknown0.A.T", m.solver.variables)
def match_Signature_against_Signature(self, sig1, sig2, subst, skip_self): # pylint: disable=invalid-name """Match a pytd.Signature against another pytd.Signature. Args: sig1: The caller sig2: The callee subst: Current type parameters. skip_self: If True, doesn't compare the first paramter, which is considered (and verified) to be "self". Returns: An instance of booleq.BooleanTerm, i.e. a boolean formula. """ assert not sig1.template assert not sig1.has_optional # Signatures have type parameters, too. We ignore them, since they can # be anything. (See maybe_lookup_type_param()) subst.update({p.type_param: None for p in sig2.template}) params2 = sig2.params params1 = sig1.params[:len(params2 )] if sig2.has_optional else sig1.params if skip_self: # Methods in an ~unknown need to declare their methods with "self" assert (params1 and params1[0].name == "self") or sig2.has_optional if params1 and params1[0].name == "self": params1 = params1[1:] # For loaded pytd, we allow methods to omit the "self" parameter. if params2 and params2[0].name == "self": params2 = params2[1:] if len(params1) == len(params2): equalities = [] for p1, p2 in zip(params1, params2): equalities.append( self.match_type_against_type(p1.type, p2.type, subst)) equalities.append( self.match_type_against_type(sig1.return_type, sig2.return_type, subst)) return booleq.And(equalities) else: return booleq.FALSE
def _TestTypeParameters(self, reverse=False): ast = parser.parse_string( textwrap.dedent(""" class `~unknown0`(): def next(self) -> ? T = TypeVar('T') class A(typing.Generic[T], object): def next(self) -> ? class B(): pass def left(x: `~unknown0`) -> ? def right(x: A[B]) -> ? """)) ast = visitors.LookupClasses(ast, self.mini_builtins) m = type_match.TypeMatch() left, right = ast.Lookup("left"), ast.Lookup("right") match = m.match(right, left, {}) if reverse else m.match( left, right, {}) self.assertEquals( match, booleq.And((booleq.Eq("~unknown0", "A"), booleq.Eq("~unknown0.A.T", "B")))) self.assertIn("~unknown0.A.T", m.solver.variables)
def match_Generic_against_Generic(self, t1, t2, subst): # pylint: disable=invalid-name """Match a pytd.GenericType against another pytd.GenericType.""" assert isinstance(t1.base_type, pytd.ClassType), type(t1.base_type) assert isinstance(t2.base_type, pytd.ClassType), type(t2.base_type) # We don't do inheritance for base types, since right now, inheriting from # instantiations of templated types is not supported by pytd. if (is_complete(t1.base_type.cls) and is_complete(t2.base_type.cls) and t1.base_type.cls.name != t2.base_type.cls.name): # Optimization: If the base types are incompatible, these two generic # types can never match. base_type_cmp = booleq.FALSE else: base_type_cmp = booleq.Eq(t1.base_type.cls.name, t2.base_type.cls.name) if base_type_cmp is booleq.FALSE: return booleq.FALSE assert len(t1.parameters) == len(t2.parameters), t1.base_type.cls.name # Type parameters are covariant: # E.g. passing list[int] as argument for list[object] succeeds. param_cmp = [ self.match_type_against_type(p1, p2, subst) for p1, p2 in zip(t1.parameters, t2.parameters) ] return booleq.And([base_type_cmp] + param_cmp)
def _match_type_against_type(self, t1, t2, subst): """Match a pytd.TYPE against another pytd.TYPE.""" t1 = self.maybe_lookup_type_param(t1, subst) t2 = self.maybe_lookup_type_param(t2, subst) # TODO(kramm): Use utils:TypeMatcher to simplify this? if isinstance(t1, pytd.AnythingType) or isinstance( t2, pytd.AnythingType): # We can match anything against AnythingType return booleq.TRUE elif isinstance(t1, pytd.NothingType) and isinstance( t2, pytd.NothingType): # nothing matches against nothing. return booleq.TRUE elif isinstance(t1, pytd.NothingType) or isinstance( t2, pytd.NothingType): # We can't match anything against nothing. (Except nothing itself, above) return booleq.FALSE elif isinstance(t1, pytd.UnionType): return booleq.And( self.match_type_against_type(u, t2, subst) for u in t1.type_list) elif isinstance(t2, pytd.UnionType): return booleq.Or( self.match_type_against_type(t1, u, subst) for u in t2.type_list) elif (isinstance(t1, pytd.ClassType) and isinstance(t2, StrictType) or isinstance(t1, StrictType) and isinstance(t2, pytd.ClassType)): # For strict types, avoid subclasses of the left side. return booleq.Eq(self._full_name(t1), self._full_name(t2)) elif isinstance(t1, pytd.ClassType): # ClassTypes are similar to Unions, except they're disjunctions: We can # match the type or any of its base classes against the formal parameter. return booleq.Or( self.match_type_against_type(t, t2, subst) for t in self.expand_superclasses(t1)) elif isinstance(t2, pytd.ClassType): # ClassTypes on the right are exactly like Unions: We can match against # this type or any of its subclasses. return booleq.Or( self.match_type_against_type(t1, t, subst) for t in self.expand_subclasses(t2)) assert not isinstance(t1, pytd.ClassType) assert not isinstance(t2, pytd.ClassType) if is_unknown(t1) and isinstance(t2, pytd.GenericType): return self.match_Unknown_against_Generic(t1, t2, subst) elif isinstance(t1, pytd.GenericType) and is_unknown(t2): return self.match_Generic_against_Unknown(t1, t2, subst) elif isinstance(t1, pytd.GenericType) and isinstance( t2, pytd.GenericType): return self.match_Generic_against_Generic(t1, t2, subst) elif isinstance(t1, pytd.GenericType): # E.g. list[...] matches against list, or even object. return self.match_type_against_type(t1.base_type, t2, subst) elif isinstance(t2, pytd.GenericType): assert t1 != t2.base_type return booleq.FALSE elif is_unknown(t1) and is_unknown(t2): return booleq.Eq(t1.name, t2.name) elif (isinstance(t1, (pytd.NamedType, StrictType)) and isinstance(t2, (pytd.NamedType, StrictType))): if is_complete(t1) and is_complete(t2) and t1.name != t2.name: # Optimization: If we know these two can never be equal, just return # false right away. return booleq.FALSE else: return booleq.Eq(t1.name, t2.name) else: raise AssertionError("Don't know how to match %s against %s" % (type(t1), type(t2)))
def match_Function_against_Function(self, f1, f2, subst, skip_self=False): # pylint: disable=invalid-name return booleq.And( self.match_Signature_against_Function(s1, f2, subst, skip_self) for s1 in f1.signatures)
def match_Signature_against_Function(self, sig, f, subst, skip_self=False): # pylint: disable=invalid-name return booleq.And( booleq.Or( self.match_Signature_against_Signature( inner, s, subst, skip_self) for s in f.signatures) for inner in sig.Visit(visitors.ExpandSignatures()))
def _match_type_against_type(self, t1, t2, subst): """Match a pytd.Type against another pytd.Type.""" t1 = self.maybe_lookup_type_param(t1, subst) t2 = self.maybe_lookup_type_param(t2, subst) # TODO(kramm): Use utils:TypeMatcher to simplify this? if isinstance(t2, pytd.AnythingType): # We can match anything against AnythingType. (It's like top) return booleq.TRUE elif isinstance(t1, pytd.AnythingType): if self.any_also_is_bottom: # We can match AnythingType against everything. (It's like bottom) return booleq.TRUE else: return booleq.FALSE elif isinstance(t1, pytd.NothingType): # nothing as an actual type matches against everything, since it # represents an empty value. return booleq.TRUE elif isinstance(t2, pytd.NothingType): # We can't match anything against nothing as an expected type (except # nothing itself, above). return booleq.FALSE elif isinstance(t1, pytd.UnionType): return booleq.And( self.match_type_against_type(u, t2, subst) for u in t1.type_list) elif isinstance(t2, pytd.UnionType): return booleq.Or( self.match_type_against_type(t1, u, subst) for u in t2.type_list) elif (isinstance(t1, pytd.ClassType) and isinstance(t2, StrictType) or isinstance(t1, StrictType) and isinstance(t2, pytd.ClassType)): # For strict types, avoid subclasses of the left side. return booleq.Eq(self._full_name(t1), self._full_name(t2)) elif (isinstance(t1, pytd.ClassType) and hasattr(t2, "name") and t2.name == "__builtin__.object"): return booleq.TRUE elif (hasattr(t1, "name") and hasattr(t2, "name") and t1.name in ("__builtin__.type", "typing.Callable") and t2.name in ("__builtin__.type", "typing.Callable")): return booleq.TRUE elif isinstance(t1, pytd.ClassType): # ClassTypes are similar to Unions, except they're disjunctions: We can # match the type or any of its base classes against the formal parameter. return booleq.Or( self.match_type_against_type(t, t2, subst) for t in self.expand_superclasses(t1)) elif isinstance(t2, pytd.ClassType): # ClassTypes on the right are exactly like Unions: We can match against # this type or any of its subclasses. return booleq.Or( self.match_type_against_type(t1, t, subst) for t in self.expand_subclasses(t2)) assert not isinstance(t1, pytd.ClassType) assert not isinstance(t2, pytd.ClassType) if is_unknown(t1) and isinstance(t2, pytd.GenericType): return self.match_Unknown_against_Generic(t1, t2, subst) elif isinstance(t1, pytd.GenericType) and is_unknown(t2): return self.match_Generic_against_Unknown(t1, t2, subst) elif isinstance(t1, pytd.GenericType) and isinstance( t2, pytd.GenericType): return self.match_Generic_against_Generic(t1, t2, subst) elif isinstance(t1, pytd.GenericType): # E.g. list[...] matches against list, or even object. return self.match_type_against_type(t1.base_type, t2, subst) elif isinstance(t2, pytd.GenericType): if self.any_also_is_bottom: # E.g. list (a.k.a. list[Any]) matches against list[str] return self.match_type_against_type(t1, t2.base_type, subst) else: return booleq.FALSE elif is_unknown(t1) and is_unknown(t2): return booleq.Eq(t1.name, t2.name) elif (isinstance(t1, (pytd.NamedType, StrictType)) and isinstance(t2, (pytd.NamedType, StrictType))): if is_complete(t1) and is_complete(t2) and t1.name != t2.name: # Optimization: If we know these two can never be equal, just return # false right away. return booleq.FALSE else: return booleq.Eq(t1.name, t2.name) elif isinstance(t1, pytd.LateType) or isinstance(t2, pytd.LateType): # Unresolved types never match against anything. return booleq.FALSE else: raise AssertionError("Don't know how to match %s against %s" % (type(t1), type(t2)))