diff --git a/python2/test_typing.py b/python2/test_typing.py index e709ffb0..b94a091c 100644 --- a/python2/test_typing.py +++ b/python2/test_typing.py @@ -132,6 +132,7 @@ def test_basic_constrained(self): def test_constrained_error(self): with self.assertRaises(TypeError): X = TypeVar('X', int) + X def test_union_unique(self): X = TypeVar('X') @@ -316,6 +317,7 @@ def test_union_instance_type_error(self): def test_union_str_pattern(self): # Shouldn't crash; see http://bugs.python.org/issue25390 A = Union[str, Pattern] + A class TypeVarUnionTests(TestCase): @@ -478,7 +480,7 @@ def get(self, key, default=None): pass -class MySimpleMapping(SimpleMapping): +class MySimpleMapping(SimpleMapping[XK, XV]): def __init__(self): self.store = {} @@ -533,14 +535,17 @@ def test_protocol_instance_type_error(self): class GenericTests(TestCase): def test_basics(self): - X = SimpleMapping[unicode, Any] - Y = SimpleMapping[XK, unicode] - X[unicode, unicode] - Y[unicode, unicode] + X = SimpleMapping[str, Any] + assert X.__parameters__ == () + with self.assertRaises(TypeError): + X[unicode] with self.assertRaises(TypeError): - X[int, unicode] + X[unicode, unicode] + Y = SimpleMapping[XK, unicode] + assert Y.__parameters__ == (XK,) + Y[unicode] with self.assertRaises(TypeError): - Y[unicode, bytes] + Y[unicode, unicode] def test_init(self): T = TypeVar('T') @@ -552,9 +557,33 @@ def test_init(self): def test_repr(self): self.assertEqual(repr(SimpleMapping), - __name__ + '.' + 'SimpleMapping[~XK, ~XV]') + __name__ + '.' + 'SimpleMapping<~XK, ~XV>') self.assertEqual(repr(MySimpleMapping), - __name__ + '.' + 'MySimpleMapping[~XK, ~XV]') + __name__ + '.' + 'MySimpleMapping<~XK, ~XV>') + + def test_chain_repr(self): + T = TypeVar('T') + S = TypeVar('S') + + class C(Generic[T]): + pass + + X = C[Tuple[S, T]] + assert X == C[Tuple[S, T]] + assert X != C[Tuple[T, S]] + + Y = X[T, int] + assert Y == X[T, int] + assert Y != X[S, int] + assert Y != X[T, str] + + Z = Y[str] + assert Z == Y[str] + assert Z != Y[int] + assert Z != Y[T] + + assert str(Z).endswith( + '.C<~T>[typing.Tuple[~S, ~T]]<~S, ~T>[~T, int]<~T>[str]') def test_dict(self): T = TypeVar('T') @@ -609,12 +638,12 @@ class C(Generic[T]): assert C.__module__ == __name__ if not PY32: assert C.__qualname__ == 'GenericTests.test_repr_2..C' - assert repr(C).split('.')[-1] == 'C[~T]' + assert repr(C).split('.')[-1] == 'C<~T>' X = C[int] assert X.__module__ == __name__ if not PY32: assert X.__qualname__ == 'C' - assert repr(X).split('.')[-1] == 'C[int]' + assert repr(X).split('.')[-1] == 'C<~T>[int]' class Y(C[int]): pass @@ -622,7 +651,7 @@ class Y(C[int]): assert Y.__module__ == __name__ if not PY32: assert Y.__qualname__ == 'GenericTests.test_repr_2..Y' - assert repr(Y).split('.')[-1] == 'Y[int]' + assert repr(Y).split('.')[-1] == 'Y' def test_eq_1(self): assert Generic == Generic @@ -650,10 +679,40 @@ class A(Generic[T, VT]): class B(Generic[KT, T]): pass - class C(A, Generic[KT, VT], B): + class C(A[T, VT], Generic[VT, T, KT], B[KT, T]): pass - assert C.__parameters__ == (T, VT, KT) + assert C.__parameters__ == (VT, T, KT) + + def test_nested(self): + + G = Generic + + class Visitor(G[T]): + + a = None + + def set(self, a): + self.a = a + + def get(self): + return self.a + + def visit(self): + return self.a + + V = Visitor[typing.List[int]] + + class IntListVisitor(V): + + def append(self, x): + self.a.append(x) + + a = IntListVisitor() + a.set([]) + a.append(1) + a.append(42) + assert a.get() == [1, 42] def test_type_erasure(self): T = TypeVar('T') @@ -679,6 +738,24 @@ def foo(x): foo(42) + def test_implicit_any(self): + T = TypeVar('T') + + class C(Generic[T]): + pass + + class D(C): + pass + + assert D.__parameters__ == () + + with self.assertRaises(Exception): + D[int] + with self.assertRaises(Exception): + D[Any] + with self.assertRaises(Exception): + D[T] + class VarianceTests(TestCase): @@ -988,6 +1065,15 @@ def test_basics(self): assert Emp._fields == ('name', 'id') assert Emp._field_types == dict(name=str, id=int) + def test_pickle(self): + global Emp # pickle wants to reference the class by name + Emp = NamedTuple('Emp', [('name', str), ('id', int)]) + jane = Emp('jane', 37) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + z = pickle.dumps(jane, proto) + jane2 = pickle.loads(z) + self.assertEqual(jane2, jane) + class IOTests(TestCase): diff --git a/python2/typing.py b/python2/typing.py index d219aebd..5e74d36b 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -1,7 +1,3 @@ -# TODO nits: -# Get rid of asserts that are the caller's fault. -# Docstrings (e.g. ABCs). - from __future__ import absolute_import, unicode_literals import abc @@ -113,8 +109,8 @@ def _eval_type(self, globalns, localns): """ return self - def _has_type_var(self): - return False + def _get_type_vars(self, tvars): + pass def __repr__(self): return '%s.%s' % (self.__module__, _qualname(self)) @@ -210,8 +206,8 @@ def __new__(cls, *args, **kwds): someone tries to subclass a type alias (not a good idea). """ if (len(args) == 3 and - isinstance(args[0], basestring) and - isinstance(args[1], tuple)): + isinstance(args[0], basestring) and + isinstance(args[1], tuple)): # Close enough. raise TypeError("A type alias cannot be subclassed") return object.__new__(cls) @@ -267,8 +263,16 @@ def __subclasscheck__(self, cls): return issubclass(cls, self.impl_type) -def _has_type_var(t): - return t is not None and isinstance(t, TypingMeta) and t._has_type_var() +def _get_type_vars(types, tvars): + for t in types: + if isinstance(t, TypingMeta): + t._get_type_vars(tvars) + + +def _type_vars(types): + tvars = [] + _get_type_vars(types, tvars) + return tuple(tvars) def _eval_type(t, globalns, localns): @@ -380,7 +384,7 @@ def longest(x: A, y: A) -> A: At runtime, isinstance(x, T) will raise TypeError. However, issubclass(C, T) is true for any class C, and issubclass(str, A) and issubclass(bytes, A) are true, and issubclass(int, A) is - false. + false. (TODO: Why is this needed? This may change. See #136.) Type variables may be marked covariant or contravariant by passing covariant=True or contravariant=True. See PEP 484 for more @@ -418,8 +422,9 @@ def __new__(cls, name, *constraints, **kwargs): self.__bound__ = None return self - def _has_type_var(self): - return True + def _get_type_vars(self, tvars): + if self not in tvars: + tvars.append(self) def __repr__(self): if self.__covariant__: @@ -456,7 +461,6 @@ def __subclasscheck__(self, cls): T_contra = TypeVar('T_contra', contravariant=True) # Ditto contravariant. # A useful type variable with constraints. This represents string types. -# TODO: What about bytearray, memoryview? AnyStr = TypeVar('AnyStr', bytes, unicode) @@ -523,12 +527,9 @@ def _eval_type(self, globalns, localns): return self.__class__(self.__name__, self.__bases__, {}, p) - def _has_type_var(self): + def _get_type_vars(self, tvars): if self.__union_params__: - for t in self.__union_params__: - if _has_type_var(t): - return True - return False + _get_type_vars(self.__union_params__, tvars) def __repr__(self): r = super(UnionMeta, self).__repr__() @@ -670,12 +671,9 @@ def __new__(cls, name, bases, namespace, parameters=None, self.__tuple_use_ellipsis__ = use_ellipsis return self - def _has_type_var(self): + def _get_type_vars(self, tvars): if self.__tuple_params__: - for t in self.__tuple_params__: - if _has_type_var(t): - return True - return False + _get_type_vars(self.__tuple_params__, tvars) def _eval_type(self, globalns, localns): tp = self.__tuple_params__ @@ -730,7 +728,8 @@ def __subclasscheck__(self, cls): if cls is Any: return True if not isinstance(cls, type): - return super(TupleMeta, self).__subclasscheck__(cls) # To TypeError. + # To TypeError. + return super(TupleMeta, self).__subclasscheck__(cls) if issubclass(cls, tuple): return True # Special case. if not isinstance(cls, TupleMeta): @@ -785,12 +784,9 @@ def __new__(cls, name, bases, namespace, self.__result__ = result return self - def _has_type_var(self): + def _get_type_vars(self, tvars): if self.__args__: - for t in self.__args__: - if _has_type_var(t): - return True - return _has_type_var(self.__result__) + _get_type_vars(self.__args__, tvars) def _eval_type(self, globalns, localns): if self.__args__ is None and self.__result__ is None: @@ -895,76 +891,106 @@ def _geqv(a, b): return _gorg(a) is _gorg(b) -class GenericMeta(TypingMeta, abc.ABCMeta): - """Metaclass for generic types.""" +def _next_in_mro(cls): + """Helper for Generic.__new__. - # TODO: Constrain more how Generic is used; only a few - # standard patterns should be allowed. + Returns the class after the last occurrence of Generic or + Generic[...] in cls.__mro__. + """ + next_in_mro = object + # Look for the last occurrence of Generic or Generic[...]. + for i, c in enumerate(cls.__mro__[:-1]): + if isinstance(c, GenericMeta) and _gorg(c) is Generic: + next_in_mro = cls.__mro__[i+1] + return next_in_mro - # TODO: Use a more precise rule than matching __name__ to decide - # whether two classes are the same. Also, save the formal - # parameters. (These things are related! A solution lies in - # using origin.) + +class GenericMeta(TypingMeta, abc.ABCMeta): + """Metaclass for generic types.""" __extra__ = None def __new__(cls, name, bases, namespace, - parameters=None, origin=None, extra=None): - if parameters is None: - # Extract parameters from direct base classes. Only - # direct bases are considered and only those that are - # themselves generic, and parameterized with type - # variables. Don't use bases like Any, Union, Tuple, - # Callable or type variables. - params = None + tvars=None, args=None, origin=None, extra=None): + self = super(GenericMeta, cls).__new__(cls, name, bases, namespace) + + if tvars is not None: + # Called from __getitem__() below. + assert origin is not None + assert all(isinstance(t, TypeVar) for t in tvars), tvars + else: + # Called from class statement. + assert tvars is None, tvars + assert args is None, args + assert origin is None, origin + + # Get the full set of tvars from the bases. + tvars = _type_vars(bases) + # Look for Generic[T1, ..., Tn]. + # If found, tvars must be a subset of it. + # If not found, tvars is it. + # Also check for and reject plain Generic, + # and reject multiple Generic[...]. + gvars = None for base in bases: - if isinstance(base, TypingMeta): - if not isinstance(base, GenericMeta): + if base is Generic: + raise TypeError("Cannot inherit from plain Generic") + if (isinstance(base, GenericMeta) and + base.__origin__ is Generic): + if gvars is not None: raise TypeError( - "You cannot inherit from magic class %s" % - repr(base)) - if base.__parameters__ is None: - continue # The base is unparameterized. - for bp in base.__parameters__: - if _has_type_var(bp) and not isinstance(bp, TypeVar): - raise TypeError( - "Cannot inherit from a generic class " - "parameterized with " - "non-type-variable %s" % bp) - if params is None: - params = [] - if bp not in params: - params.append(bp) - if params is not None: - parameters = tuple(params) - self = super(GenericMeta, cls).__new__(cls, name, bases, namespace) - self.__parameters__ = parameters + "Cannot inherit from Generic[...] multiple types.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + raise TypeError( + "Some type variables (%s) " + "are not listed in Generic[%s]" % + (", ".join(str(t) for t in tvars if t not in gvarset), + ", ".join(str(g) for g in gvars))) + tvars = gvars + + self.__parameters__ = tvars + self.__args__ = args + self.__origin__ = origin if extra is not None: self.__extra__ = extra # Else __extra__ is inherited, eventually from the # (meta-)class default above. - self.__origin__ = origin + # Speed hack (https://github.com/python/typing/issues/196). + self.__next_in_mro__ = _next_in_mro(self) return self - def _has_type_var(self): - if self.__parameters__: - for t in self.__parameters__: - if _has_type_var(t): - return True - return False + def _get_type_vars(self, tvars): + if self.__origin__ and self.__parameters__: + _get_type_vars(self.__parameters__, tvars) def __repr__(self): - r = super(GenericMeta, self).__repr__() - if self.__parameters__ is not None: + if self.__origin__ is not None: + r = repr(self.__origin__) + else: + r = super(GenericMeta, self).__repr__() + if self.__args__: r += '[%s]' % ( + ', '.join(_type_repr(p) for p in self.__args__)) + if self.__parameters__: + r += '<%s>' % ( ', '.join(_type_repr(p) for p in self.__parameters__)) return r def __eq__(self, other): if not isinstance(other, GenericMeta): return NotImplemented - return (_geqv(self, other) and - self.__parameters__ == other.__parameters__) + if self.__origin__ is not None: + return (self.__origin__ is other.__origin__ and + self.__args__ == other.__args__ and + self.__parameters__ == other.__parameters__) + else: + return self is other def __hash__(self): return hash((self.__name__, self.__parameters__)) @@ -973,37 +999,45 @@ def __getitem__(self, params): if not isinstance(params, tuple): params = (params,) if not params: - raise TypeError("Cannot have empty parameter list") + raise TypeError( + "Parameter list to %s[...] cannot be empty" % _qualname(self)) msg = "Parameters to generic types must be types." params = tuple(_type_check(p, msg) for p in params) - if self.__parameters__ is None: - for p in params: - if not isinstance(p, TypeVar): - raise TypeError("Initial parameters must be " - "type variables; got %s" % p) + if self is Generic: + # Generic can only be subscripted with unique type variables. + if not all(isinstance(p, TypeVar) for p in params): + raise TypeError( + "Parameters to Generic[...] must all be type variables") if len(set(params)) != len(params): raise TypeError( - "All type variables in Generic[...] must be distinct.") + "Parameters to Generic[...] must all be unique") + tvars = params + args = None + elif self is _Protocol: + # _Protocol is internal, don't check anything. + tvars = params + args = None + elif self.__origin__ in (Generic, _Protocol): + # Can't subscript Generic[...] or _Protocol[...]. + raise TypeError("Cannot subscript already-subscripted %s" % + repr(self)) else: - if len(params) != len(self.__parameters__): - raise TypeError("Cannot change parameter count from %d to %d" % - (len(self.__parameters__), len(params))) - for new, old in zip(params, self.__parameters__): - if isinstance(old, TypeVar): - if not old.__constraints__: - # Substituting for an unconstrained TypeVar is OK. - continue - if issubclass(new, Union[old.__constraints__]): - # Specializing a constrained type variable is OK. - continue - if not issubclass(new, old): - raise TypeError( - "Cannot substitute %s for %s in %s" % - (_type_repr(new), _type_repr(old), self)) - - return self.__class__(self.__name__, (self,) + self.__bases__, + # Subscripting a regular Generic subclass. + if not self.__parameters__: + raise TypeError("%s is not a generic class" % repr(self)) + alen = len(params) + elen = len(self.__parameters__) + if alen != elen: + raise TypeError( + "Too %s parameters for %s; actual %s, expected %s" % + ("many" if alen > elen else "few", repr(self), alen, elen)) + tvars = _type_vars(params) + args = params + return self.__class__(self.__name__, + (self,) + self.__bases__, dict(self.__dict__), - parameters=params, + tvars=tvars, + args=args, origin=self, extra=self.__extra__) @@ -1023,10 +1057,10 @@ def __subclasscheck__(self, cls): # C[X] is a subclass of C[Y] iff X is a subclass of Y. origin = self.__origin__ if origin is not None and origin is cls.__origin__: - assert len(self.__parameters__) == len(origin.__parameters__) - assert len(cls.__parameters__) == len(origin.__parameters__) - for p_self, p_cls, p_origin in zip(self.__parameters__, - cls.__parameters__, + assert len(self.__args__) == len(origin.__parameters__) + assert len(cls.__args__) == len(origin.__parameters__) + for p_self, p_cls, p_origin in zip(self.__args__, + cls.__args__, origin.__parameters__): if isinstance(p_origin, TypeVar): if p_origin.__covariant__: @@ -1056,6 +1090,10 @@ def __subclasscheck__(self, cls): return issubclass(cls, self.__extra__) +# Prevent checks for Generic to crash when defining Generic. +Generic = None + + class Generic(object): """Abstract base class for generic types. @@ -1070,34 +1108,24 @@ def __getitem__(self, key: KT) -> VT: This class can then be used as follows:: - def lookup_name(mapping: Mapping, key: KT, default: VT) -> VT: + def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT: try: return mapping[key] except KeyError: return default - - For clarity the type variables may be redefined, e.g.:: - - X = TypeVar('X') - Y = TypeVar('Y') - def lookup_name(mapping: Mapping[X, Y], key: X, default: Y) -> Y: - # Same body as above. """ __metaclass__ = GenericMeta __slots__ = () def __new__(cls, *args, **kwds): - next_in_mro = object - # Look for the last occurrence of Generic or Generic[...]. - for i, c in enumerate(cls.__mro__[:-1]): - if isinstance(c, GenericMeta) and _gorg(c) is Generic: - next_in_mro = cls.__mro__[i+1] - origin = _gorg(cls) - obj = next_in_mro.__new__(origin) - if origin is not cls: + if cls.__origin__ is None: + return cls.__next_in_mro__.__new__(cls) + else: + origin = _gorg(cls) + obj = cls.__next_in_mro__.__new__(origin) obj.__init__(*args, **kwds) - return obj + return obj def cast(typ, val): @@ -1115,9 +1143,7 @@ def _get_defaults(func): """Internal helper to extract the default arguments, by name.""" code = func.__code__ pos_count = code.co_argcount - kw_count = code.co_kwonlyargcount arg_names = code.co_varnames - kwarg_names = arg_names[pos_count:pos_count + kw_count] arg_names = arg_names[:pos_count] defaults = func.__defaults__ or () kwdefaults = func.__kwdefaults__ @@ -1170,7 +1196,6 @@ def get_type_hints(obj, globalns=None, localns=None): return hints -# TODO: Also support this as a class decorator. def no_type_check(arg): """Decorator to indicate that annotations are not type hints. @@ -1288,14 +1313,16 @@ def _get_protocol_attrs(self): break else: if (not attr.startswith('_abc_') and - attr != '__abstractmethods__' and - attr != '_is_protocol' and - attr != '__dict__' and - attr != '__slots__' and - attr != '_get_protocol_attrs' and - attr != '__parameters__' and - attr != '__origin__' and - attr != '__module__'): + attr != '__abstractmethods__' and + attr != '_is_protocol' and + attr != '__dict__' and + attr != '__args__' and + attr != '__slots__' and + attr != '_get_protocol_attrs' and + attr != '__next_in_mro__' and + attr != '__parameters__' and + attr != '__origin__' and + attr != '__module__'): attrs.add(attr) return attrs @@ -1391,7 +1418,7 @@ class MutableSet(AbstractSet[T]): # NOTE: Only the value type is covariant. -class Mapping(Sized, Iterable[KT], Container[KT], Generic[VT_co]): +class Mapping(Sized, Iterable[KT], Container[KT], Generic[KT, VT_co]): __extra__ = collections_abc.Mapping @@ -1465,8 +1492,9 @@ class KeysView(MappingView[KT], AbstractSet[KT]): __extra__ = collections_abc.KeysView -# TODO: Enable Set[Tuple[KT, VT_co]] instead of Generic[KT, VT_co]. -class ItemsView(MappingView, Generic[KT, VT_co]): +class ItemsView(MappingView[Tuple[KT, VT_co]], + Set[Tuple[KT, VT_co]], + Generic[KT, VT_co]): __extra__ = collections_abc.ItemsView @@ -1522,6 +1550,11 @@ def NamedTuple(typename, fields): fields = [(n, t) for n, t in fields] cls = collections.namedtuple(typename, [n for n, t in fields]) cls._field_types = dict(fields) + # Set the module to the caller's module (otherwise it'd be 'typing'). + try: + cls.__module__ = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass return cls diff --git a/src/test_typing.py b/src/test_typing.py index 0f40b93a..c7b518d9 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -132,6 +132,7 @@ def test_basic_constrained(self): def test_constrained_error(self): with self.assertRaises(TypeError): X = TypeVar('X', int) + X def test_union_unique(self): X = TypeVar('X') @@ -316,6 +317,7 @@ def test_union_instance_type_error(self): def test_union_str_pattern(self): # Shouldn't crash; see http://bugs.python.org/issue25390 A = Union[str, Pattern] + A class TypeVarUnionTests(TestCase): @@ -486,7 +488,7 @@ def get(self, key: XK, default: XV = None) -> XV: ... -class MySimpleMapping(SimpleMapping): +class MySimpleMapping(SimpleMapping[XK, XV]): def __init__(self): self.store = {} @@ -540,6 +542,7 @@ def test_supports_abs(self): assert not issubclass(str, typing.SupportsAbs) def test_supports_round(self): + issubclass(float, typing.SupportsRound) assert issubclass(float, typing.SupportsRound) assert issubclass(int, typing.SupportsRound) assert not issubclass(str, typing.SupportsRound) @@ -557,13 +560,16 @@ class GenericTests(TestCase): def test_basics(self): X = SimpleMapping[str, Any] - Y = SimpleMapping[XK, str] - X[str, str] - Y[str, str] + assert X.__parameters__ == () + with self.assertRaises(TypeError): + X[str] with self.assertRaises(TypeError): - X[int, str] + X[str, str] + Y = SimpleMapping[XK, str] + assert Y.__parameters__ == (XK,) + Y[str] with self.assertRaises(TypeError): - Y[str, bytes] + Y[str, str] def test_init(self): T = TypeVar('T') @@ -575,9 +581,33 @@ def test_init(self): def test_repr(self): self.assertEqual(repr(SimpleMapping), - __name__ + '.' + 'SimpleMapping[~XK, ~XV]') + __name__ + '.' + 'SimpleMapping<~XK, ~XV>') self.assertEqual(repr(MySimpleMapping), - __name__ + '.' + 'MySimpleMapping[~XK, ~XV]') + __name__ + '.' + 'MySimpleMapping<~XK, ~XV>') + + def test_chain_repr(self): + T = TypeVar('T') + S = TypeVar('S') + + class C(Generic[T]): + pass + + X = C[Tuple[S, T]] + assert X == C[Tuple[S, T]] + assert X != C[Tuple[T, S]] + + Y = X[T, int] + assert Y == X[T, int] + assert Y != X[S, int] + assert Y != X[T, str] + + Z = Y[str] + assert Z == Y[str] + assert Z != Y[int] + assert Z != Y[T] + + assert str(Z).endswith( + '.C<~T>[typing.Tuple[~S, ~T]]<~S, ~T>[~T, int]<~T>[str]') def test_dict(self): T = TypeVar('T') @@ -632,12 +662,12 @@ class C(Generic[T]): assert C.__module__ == __name__ if not PY32: assert C.__qualname__ == 'GenericTests.test_repr_2..C' - assert repr(C).split('.')[-1] == 'C[~T]' + assert repr(C).split('.')[-1] == 'C<~T>' X = C[int] assert X.__module__ == __name__ if not PY32: assert X.__qualname__ == 'C' - assert repr(X).split('.')[-1] == 'C[int]' + assert repr(X).split('.')[-1] == 'C<~T>[int]' class Y(C[int]): pass @@ -645,7 +675,7 @@ class Y(C[int]): assert Y.__module__ == __name__ if not PY32: assert Y.__qualname__ == 'GenericTests.test_repr_2..Y' - assert repr(Y).split('.')[-1] == 'Y[int]' + assert repr(Y).split('.')[-1] == 'Y' def test_eq_1(self): assert Generic == Generic @@ -673,15 +703,14 @@ class A(Generic[T, VT]): class B(Generic[KT, T]): pass - class C(A, Generic[KT, VT], B): + class C(A[T, VT], Generic[VT, T, KT], B[KT, T]): pass - assert C.__parameters__ == (T, VT, KT) + assert C.__parameters__ == (VT, T, KT) def test_nested(self): - class G(Generic): - pass + G = Generic class Visitor(G[T]): @@ -733,6 +762,24 @@ def foo(x: T): foo(42) + def test_implicit_any(self): + T = TypeVar('T') + + class C(Generic[T]): + pass + + class D(C): + pass + + assert D.__parameters__ == () + + with self.assertRaises(Exception): + D[int] + with self.assertRaises(Exception): + D[Any] + with self.assertRaises(Exception): + D[T] + class VarianceTests(TestCase): @@ -1289,7 +1336,7 @@ def stuff(a: TextIO) -> str: return a.readline() a = stuff.__annotations__['a'] - assert a.__parameters__ == (str,) + assert a.__parameters__ == () def test_binaryio(self): @@ -1297,7 +1344,7 @@ def stuff(a: BinaryIO) -> bytes: return a.readline() a = stuff.__annotations__['a'] - assert a.__parameters__ == (bytes,) + assert a.__parameters__ == () def test_io_submodule(self): from typing.io import IO, TextIO, BinaryIO, __all__, __name__ diff --git a/src/typing.py b/src/typing.py index a1469ff0..d6f64bbc 100644 --- a/src/typing.py +++ b/src/typing.py @@ -1,7 +1,3 @@ -# TODO nits: -# Get rid of asserts that are the caller's fault. -# Docstrings (e.g. ABCs). - import abc from abc import abstractmethod, abstractproperty import collections @@ -117,8 +113,8 @@ def _eval_type(self, globalns, localns): """ return self - def _has_type_var(self): - return False + def _get_type_vars(self, tvars): + pass def __repr__(self): return '%s.%s' % (self.__module__, _qualname(self)) @@ -214,8 +210,8 @@ def __new__(cls, *args, **kwds): someone tries to subclass a type alias (not a good idea). """ if (len(args) == 3 and - isinstance(args[0], str) and - isinstance(args[1], tuple)): + isinstance(args[0], str) and + isinstance(args[1], tuple)): # Close enough. raise TypeError("A type alias cannot be subclassed") return object.__new__(cls) @@ -271,8 +267,16 @@ def __subclasscheck__(self, cls): return issubclass(cls, self.impl_type) -def _has_type_var(t): - return t is not None and isinstance(t, TypingMeta) and t._has_type_var() +def _get_type_vars(types, tvars): + for t in types: + if isinstance(t, TypingMeta): + t._get_type_vars(tvars) + + +def _type_vars(types): + tvars = [] + _get_type_vars(types, tvars) + return tuple(tvars) def _eval_type(t, globalns, localns): @@ -376,7 +380,7 @@ def longest(x: A, y: A) -> A: At runtime, isinstance(x, T) will raise TypeError. However, issubclass(C, T) is true for any class C, and issubclass(str, A) and issubclass(bytes, A) are true, and issubclass(int, A) is - false. + false. (TODO: Why is this needed? This may change. See #136.) Type variables may be marked covariant or contravariant by passing covariant=True or contravariant=True. See PEP 484 for more @@ -410,8 +414,9 @@ def __new__(cls, name, *constraints, bound=None, self.__bound__ = None return self - def _has_type_var(self): - return True + def _get_type_vars(self, tvars): + if self not in tvars: + tvars.append(self) def __repr__(self): if self.__covariant__: @@ -448,7 +453,6 @@ def __subclasscheck__(self, cls): T_contra = TypeVar('T_contra', contravariant=True) # Ditto contravariant. # A useful type variable with constraints. This represents string types. -# TODO: What about bytearray, memoryview? AnyStr = TypeVar('AnyStr', bytes, str) @@ -514,12 +518,9 @@ def _eval_type(self, globalns, localns): return self.__class__(self.__name__, self.__bases__, {}, p, _root=True) - def _has_type_var(self): + def _get_type_vars(self, tvars): if self.__union_params__: - for t in self.__union_params__: - if _has_type_var(t): - return True - return False + _get_type_vars(self.__union_params__, tvars) def __repr__(self): r = super().__repr__() @@ -656,12 +657,9 @@ def __new__(cls, name, bases, namespace, parameters=None, self.__tuple_use_ellipsis__ = use_ellipsis return self - def _has_type_var(self): + def _get_type_vars(self, tvars): if self.__tuple_params__: - for t in self.__tuple_params__: - if _has_type_var(t): - return True - return False + _get_type_vars(self.__tuple_params__, tvars) def _eval_type(self, globalns, localns): tp = self.__tuple_params__ @@ -769,12 +767,9 @@ def __new__(cls, name, bases, namespace, _root=False, self.__result__ = result return self - def _has_type_var(self): + def _get_type_vars(self, tvars): if self.__args__: - for t in self.__args__: - if _has_type_var(t): - return True - return _has_type_var(self.__result__) + _get_type_vars(self.__args__, tvars) def _eval_type(self, globalns, localns): if self.__args__ is None and self.__result__ is None: @@ -878,76 +873,106 @@ def _geqv(a, b): return _gorg(a) is _gorg(b) -class GenericMeta(TypingMeta, abc.ABCMeta): - """Metaclass for generic types.""" +def _next_in_mro(cls): + """Helper for Generic.__new__. - # TODO: Constrain more how Generic is used; only a few - # standard patterns should be allowed. + Returns the class after the last occurrence of Generic or + Generic[...] in cls.__mro__. + """ + next_in_mro = object + # Look for the last occurrence of Generic or Generic[...]. + for i, c in enumerate(cls.__mro__[:-1]): + if isinstance(c, GenericMeta) and _gorg(c) is Generic: + next_in_mro = cls.__mro__[i+1] + return next_in_mro - # TODO: Use a more precise rule than matching __name__ to decide - # whether two classes are the same. Also, save the formal - # parameters. (These things are related! A solution lies in - # using origin.) + +class GenericMeta(TypingMeta, abc.ABCMeta): + """Metaclass for generic types.""" __extra__ = None def __new__(cls, name, bases, namespace, - parameters=None, origin=None, extra=None): - if parameters is None: - # Extract parameters from direct base classes. Only - # direct bases are considered and only those that are - # themselves generic, and parameterized with type - # variables. Don't use bases like Any, Union, Tuple, - # Callable or type variables. - params = None + tvars=None, args=None, origin=None, extra=None): + self = super().__new__(cls, name, bases, namespace, _root=True) + + if tvars is not None: + # Called from __getitem__() below. + assert origin is not None + assert all(isinstance(t, TypeVar) for t in tvars), tvars + else: + # Called from class statement. + assert tvars is None, tvars + assert args is None, args + assert origin is None, origin + + # Get the full set of tvars from the bases. + tvars = _type_vars(bases) + # Look for Generic[T1, ..., Tn]. + # If found, tvars must be a subset of it. + # If not found, tvars is it. + # Also check for and reject plain Generic, + # and reject multiple Generic[...]. + gvars = None for base in bases: - if isinstance(base, TypingMeta): - if not isinstance(base, GenericMeta): + if base is Generic: + raise TypeError("Cannot inherit from plain Generic") + if (isinstance(base, GenericMeta) and + base.__origin__ is Generic): + if gvars is not None: raise TypeError( - "You cannot inherit from magic class %s" % - repr(base)) - if base.__parameters__ is None: - continue # The base is unparameterized. - for bp in base.__parameters__: - if _has_type_var(bp) and not isinstance(bp, TypeVar): - raise TypeError( - "Cannot inherit from a generic class " - "parameterized with " - "non-type-variable %s" % bp) - if params is None: - params = [] - if bp not in params: - params.append(bp) - if params is not None: - parameters = tuple(params) - self = super().__new__(cls, name, bases, namespace, _root=True) - self.__parameters__ = parameters + "Cannot inherit from Generic[...] multiple types.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + raise TypeError( + "Some type variables (%s) " + "are not listed in Generic[%s]" % + (", ".join(str(t) for t in tvars if t not in gvarset), + ", ".join(str(g) for g in gvars))) + tvars = gvars + + self.__parameters__ = tvars + self.__args__ = args + self.__origin__ = origin if extra is not None: self.__extra__ = extra # Else __extra__ is inherited, eventually from the # (meta-)class default above. - self.__origin__ = origin + # Speed hack (https://github.com/python/typing/issues/196). + self.__next_in_mro__ = _next_in_mro(self) return self - def _has_type_var(self): - if self.__parameters__: - for t in self.__parameters__: - if _has_type_var(t): - return True - return False + def _get_type_vars(self, tvars): + if self.__origin__ and self.__parameters__: + _get_type_vars(self.__parameters__, tvars) def __repr__(self): - r = super().__repr__() - if self.__parameters__ is not None: + if self.__origin__ is not None: + r = repr(self.__origin__) + else: + r = super().__repr__() + if self.__args__: r += '[%s]' % ( + ', '.join(_type_repr(p) for p in self.__args__)) + if self.__parameters__: + r += '<%s>' % ( ', '.join(_type_repr(p) for p in self.__parameters__)) return r def __eq__(self, other): if not isinstance(other, GenericMeta): return NotImplemented - return (_geqv(self, other) and - self.__parameters__ == other.__parameters__) + if self.__origin__ is not None: + return (self.__origin__ is other.__origin__ and + self.__args__ == other.__args__ and + self.__parameters__ == other.__parameters__) + else: + return self is other def __hash__(self): return hash((self.__name__, self.__parameters__)) @@ -956,37 +981,45 @@ def __getitem__(self, params): if not isinstance(params, tuple): params = (params,) if not params: - raise TypeError("Cannot have empty parameter list") + raise TypeError( + "Parameter list to %s[...] cannot be empty" % _qualname(self)) msg = "Parameters to generic types must be types." params = tuple(_type_check(p, msg) for p in params) - if self.__parameters__ is None: - for p in params: - if not isinstance(p, TypeVar): - raise TypeError("Initial parameters must be " - "type variables; got %s" % p) + if self is Generic: + # Generic can only be subscripted with unique type variables. + if not all(isinstance(p, TypeVar) for p in params): + raise TypeError( + "Parameters to Generic[...] must all be type variables") if len(set(params)) != len(params): raise TypeError( - "All type variables in Generic[...] must be distinct.") + "Parameters to Generic[...] must all be unique") + tvars = params + args = None + elif self is _Protocol: + # _Protocol is internal, don't check anything. + tvars = params + args = None + elif self.__origin__ in (Generic, _Protocol): + # Can't subscript Generic[...] or _Protocol[...]. + raise TypeError("Cannot subscript already-subscripted %s" % + repr(self)) else: - if len(params) != len(self.__parameters__): - raise TypeError("Cannot change parameter count from %d to %d" % - (len(self.__parameters__), len(params))) - for new, old in zip(params, self.__parameters__): - if isinstance(old, TypeVar): - if not old.__constraints__: - # Substituting for an unconstrained TypeVar is OK. - continue - if issubclass(new, Union[old.__constraints__]): - # Specializing a constrained type variable is OK. - continue - if not issubclass(new, old): - raise TypeError( - "Cannot substitute %s for %s in %s" % - (_type_repr(new), _type_repr(old), self)) - - return self.__class__(self.__name__, (self,) + self.__bases__, + # Subscripting a regular Generic subclass. + if not self.__parameters__: + raise TypeError("%s is not a generic class" % repr(self)) + alen = len(params) + elen = len(self.__parameters__) + if alen != elen: + raise TypeError( + "Too %s parameters for %s; actual %s, expected %s" % + ("many" if alen > elen else "few", repr(self), alen, elen)) + tvars = _type_vars(params) + args = params + return self.__class__(self.__name__, + (self,) + self.__bases__, dict(self.__dict__), - parameters=params, + tvars=tvars, + args=args, origin=self, extra=self.__extra__) @@ -1006,10 +1039,10 @@ def __subclasscheck__(self, cls): # C[X] is a subclass of C[Y] iff X is a subclass of Y. origin = self.__origin__ if origin is not None and origin is cls.__origin__: - assert len(self.__parameters__) == len(origin.__parameters__) - assert len(cls.__parameters__) == len(origin.__parameters__) - for p_self, p_cls, p_origin in zip(self.__parameters__, - cls.__parameters__, + assert len(self.__args__) == len(origin.__parameters__) + assert len(cls.__args__) == len(origin.__parameters__) + for p_self, p_cls, p_origin in zip(self.__args__, + cls.__args__, origin.__parameters__): if isinstance(p_origin, TypeVar): if p_origin.__covariant__: @@ -1039,6 +1072,10 @@ def __subclasscheck__(self, cls): return issubclass(cls, self.__extra__) +# Prevent checks for Generic to crash when defining Generic. +Generic = None + + class Generic(metaclass=GenericMeta): """Abstract base class for generic types. @@ -1053,33 +1090,23 @@ def __getitem__(self, key: KT) -> VT: This class can then be used as follows:: - def lookup_name(mapping: Mapping, key: KT, default: VT) -> VT: + def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT: try: return mapping[key] except KeyError: return default - - For clarity the type variables may be redefined, e.g.:: - - X = TypeVar('X') - Y = TypeVar('Y') - def lookup_name(mapping: Mapping[X, Y], key: X, default: Y) -> Y: - # Same body as above. """ __slots__ = () def __new__(cls, *args, **kwds): - next_in_mro = object - # Look for the last occurrence of Generic or Generic[...]. - for i, c in enumerate(cls.__mro__[:-1]): - if isinstance(c, GenericMeta) and _gorg(c) is Generic: - next_in_mro = cls.__mro__[i+1] - origin = _gorg(cls) - obj = next_in_mro.__new__(origin) - if origin is not cls: + if cls.__origin__ is None: + return cls.__next_in_mro__.__new__(cls) + else: + origin = _gorg(cls) + obj = cls.__next_in_mro__.__new__(origin) obj.__init__(*args, **kwds) - return obj + return obj def cast(typ, val): @@ -1097,9 +1124,7 @@ def _get_defaults(func): """Internal helper to extract the default arguments, by name.""" code = func.__code__ pos_count = code.co_argcount - kw_count = code.co_kwonlyargcount arg_names = code.co_varnames - kwarg_names = arg_names[pos_count:pos_count + kw_count] arg_names = arg_names[:pos_count] defaults = func.__defaults__ or () kwdefaults = func.__kwdefaults__ @@ -1152,7 +1177,6 @@ def get_type_hints(obj, globalns=None, localns=None): return hints -# TODO: Also support this as a class decorator. def no_type_check(arg): """Decorator to indicate that annotations are not type hints. @@ -1270,14 +1294,16 @@ def _get_protocol_attrs(self): break else: if (not attr.startswith('_abc_') and - attr != '__abstractmethods__' and - attr != '_is_protocol' and - attr != '__dict__' and - attr != '__slots__' and - attr != '_get_protocol_attrs' and - attr != '__parameters__' and - attr != '__origin__' and - attr != '__module__'): + attr != '__abstractmethods__' and + attr != '_is_protocol' and + attr != '__dict__' and + attr != '__args__' and + attr != '__slots__' and + attr != '_get_protocol_attrs' and + attr != '__next_in_mro__' and + attr != '__parameters__' and + attr != '__origin__' and + attr != '__module__'): attrs.add(attr) return attrs @@ -1314,7 +1340,8 @@ class Awaitable(Generic[T_co], extra=collections_abc.Awaitable): class AsyncIterable(Generic[T_co], extra=collections_abc.AsyncIterable): __slots__ = () - class AsyncIterator(AsyncIterable[T_co], extra=collections_abc.AsyncIterator): + class AsyncIterator(AsyncIterable[T_co], + extra=collections_abc.AsyncIterator): __slots__ = () else: @@ -1406,7 +1433,7 @@ class MutableSet(AbstractSet[T], extra=collections_abc.MutableSet): # NOTE: Only the value type is covariant. -class Mapping(Sized, Iterable[KT], Container[KT], Generic[VT_co], +class Mapping(Sized, Iterable[KT], Container[KT], Generic[KT, VT_co], extra=collections_abc.Mapping): pass @@ -1482,8 +1509,9 @@ class KeysView(MappingView[KT], AbstractSet[KT], pass -# TODO: Enable Set[Tuple[KT, VT_co]] instead of Generic[KT, VT_co]. -class ItemsView(MappingView, Generic[KT, VT_co], +class ItemsView(MappingView[Tuple[KT, VT_co]], + Set[Tuple[KT, VT_co]], + Generic[KT, VT_co], extra=collections_abc.ItemsView): pass diff --git a/tox.ini b/tox.ini index 187303aa..079f02c3 100644 --- a/tox.ini +++ b/tox.ini @@ -9,4 +9,7 @@ commands = python -m unittest discover changedir = python2 [pep8] -ignore = E129,E226 +ignore = E129,E226,E251 + +[flake8] +ignore = DW12,E226,E251,F401,F811