diff --git a/python/src/META-INF/python-core.xml b/python/src/META-INF/python-core.xml
index 292237aac6ac..7adbc46c189c 100644
--- a/python/src/META-INF/python-core.xml
+++ b/python/src/META-INF/python-core.xml
@@ -589,6 +589,9 @@
+
+
+
diff --git a/python/src/com/jetbrains/python/codeInsight/PyTypingTypeProvider.java b/python/src/com/jetbrains/python/codeInsight/PyTypingTypeProvider.java
new file mode 100644
index 000000000000..7ee234b546cf
--- /dev/null
+++ b/python/src/com/jetbrains/python/codeInsight/PyTypingTypeProvider.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2000-2014 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.python.codeInsight;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiPolyVariantReference;
+import com.jetbrains.python.psi.*;
+import com.jetbrains.python.psi.resolve.PyResolveContext;
+import com.jetbrains.python.psi.types.*;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author vlan
+ */
+public class PyTypingTypeProvider extends PyTypeProviderBase {
+ @Override
+ public PyType getParameterType(@NotNull PyNamedParameter param, @NotNull PyFunction func, @NotNull TypeEvalContext context) {
+ final PyAnnotation annotation = param.getAnnotation();
+ if (annotation != null) {
+ final PyExpression value = annotation.getValue();
+ if (value != null) {
+ return getTypingType(value, context);
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public PyType getReturnType(@NotNull Callable callable, @NotNull TypeEvalContext context) {
+ if (callable instanceof PyFunction) {
+ final PyFunction function = (PyFunction)callable;
+ final PyAnnotation annotation = function.getAnnotation();
+ if (annotation != null) {
+ final PyExpression value = annotation.getValue();
+ if (value != null) {
+ return getTypingType(value, context);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static PyType getTypingType(@NotNull PyExpression expression, @NotNull TypeEvalContext context) {
+ // TODO: Put the annotation text into stubs and parse it to avoid switching from stubs to AST
+ if (expression instanceof PySubscriptionExpression) {
+ final PySubscriptionExpression subscriptionExpr = (PySubscriptionExpression)expression;
+ final PyExpression operand = subscriptionExpr.getOperand();
+ final String operandName = resolveToQualifiedName(operand, context);
+ if ("typing.Union".equals(operandName)) {
+ final PyExpression indexExpr = subscriptionExpr.getIndexExpression();
+ if (indexExpr instanceof PyTupleExpression) {
+ final PyTupleExpression tupleExpr = (PyTupleExpression)indexExpr;
+ final List types = new ArrayList();
+ for (PyExpression expr : tupleExpr.getElements()) {
+ types.add(getType(expr, context));
+ }
+ return PyUnionType.union(types);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static PyType getType(@NotNull PyExpression expression, @NotNull TypeEvalContext context) {
+ final PyType typingType = getTypingType(expression, context);
+ if (typingType != null) {
+ return typingType;
+ }
+ final PyType type = context.getType(expression);
+ if (type instanceof PyClassLikeType) {
+ final PyClassLikeType classType = (PyClassLikeType)type;
+ if (classType.isDefinition()) {
+ return classType.toInstance();
+ }
+ }
+ else if (type instanceof PyNoneType) {
+ return type;
+ }
+ return null;
+ }
+
+ @Nullable
+ private static String resolveToQualifiedName(@NotNull PyExpression expression, @NotNull TypeEvalContext context) {
+ if (expression instanceof PyReferenceOwner) {
+ final PyReferenceOwner referenceOwner = (PyReferenceOwner)expression;
+ final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context);
+ final PsiPolyVariantReference reference = referenceOwner.getReference(resolveContext);
+ final PsiElement element = reference.resolve();
+ if (element instanceof PyQualifiedNameOwner) {
+ final PyQualifiedNameOwner qualifiedNameOwner = (PyQualifiedNameOwner)element;
+ return qualifiedNameOwner.getQualifiedName();
+ }
+ }
+ return null;
+ }
+}
diff --git a/python/testData/typing/typing.py b/python/testData/typing/typing.py
index e69de29bb2d1..1fbd5fc8081b 100644
--- a/python/testData/typing/typing.py
+++ b/python/testData/typing/typing.py
@@ -0,0 +1,582 @@
+"""Static type checking helpers"""
+
+from abc import ABCMeta, abstractmethod, abstractproperty
+import inspect
+import sys
+import re
+
+
+__all__ = [
+ # Type system related
+ 'AbstractGeneric',
+ 'AbstractGenericMeta',
+ 'Any',
+ 'AnyStr',
+ 'Dict',
+ 'Function',
+ 'Generic',
+ 'GenericMeta',
+ 'IO',
+ 'List',
+ 'Match',
+ 'Pattern',
+ 'Protocol',
+ 'Set',
+ 'Tuple',
+ 'Undefined',
+ 'Union',
+ 'cast',
+ 'forwardref',
+ 'overload',
+ 'typevar',
+ # Protocols and abstract base classes
+ 'Container',
+ 'Iterable',
+ 'Iterator',
+ 'Sequence',
+ 'Sized',
+ 'AbstractSet',
+ 'Mapping',
+ 'BinaryIO',
+ 'TextIO',
+]
+
+
+def builtinclass(cls):
+ """Mark a class as a built-in/extension class for type checking."""
+ return cls
+
+
+def ducktype(type):
+ """Return a duck type declaration decorator.
+
+ The decorator only affects type checking.
+ """
+ def decorator(cls):
+ return cls
+ return decorator
+
+
+def disjointclass(type):
+ """Return a disjoint class declaration decorator.
+
+ The decorator only affects type checking.
+ """
+ def decorator(cls):
+ return cls
+ return decorator
+
+
+class GenericMeta(type):
+ """Metaclass for generic classes that support indexing by types."""
+
+ def __getitem__(self, args):
+ # Just ignore args; they are for compile-time checks only.
+ return self
+
+
+class Generic(metaclass=GenericMeta):
+ """Base class for generic classes."""
+
+
+class AbstractGenericMeta(ABCMeta):
+ """Metaclass for abstract generic classes that support type indexing.
+
+ This is used for both protocols and ordinary abstract classes.
+ """
+
+ def __new__(mcls, name, bases, namespace):
+ cls = super().__new__(mcls, name, bases, namespace)
+ # 'Protocol' must be an explicit base class in order for a class to
+ # be a protocol.
+ cls._is_protocol = name == 'Protocol' or Protocol in bases
+ return cls
+
+ def __getitem__(self, args):
+ # Just ignore args; they are for compile-time checks only.
+ return self
+
+
+class Protocol(metaclass=AbstractGenericMeta):
+ """Base class for protocol classes."""
+
+ @classmethod
+ def __subclasshook__(cls, c):
+ if not cls._is_protocol:
+ # No structural checks since this isn't a protocol.
+ return NotImplemented
+
+ if cls is Protocol:
+ # Every class is a subclass of the empty protocol.
+ return True
+
+ # Find all attributes defined in the protocol.
+ attrs = cls._get_protocol_attrs()
+
+ for attr in attrs:
+ if not any(attr in d.__dict__ for d in c.__mro__):
+ return NotImplemented
+ return True
+
+ @classmethod
+ def _get_protocol_attrs(cls):
+ # Get all Protocol base classes.
+ protocol_bases = []
+ for c in cls.__mro__:
+ if getattr(c, '_is_protocol', False) and c.__name__ != 'Protocol':
+ protocol_bases.append(c)
+
+ # Get attributes included in protocol.
+ attrs = set()
+ for base in protocol_bases:
+ for attr in base.__dict__.keys():
+ # Include attributes not defined in any non-protocol bases.
+ for c in cls.__mro__:
+ if (c is not base and attr in c.__dict__ and
+ not getattr(c, '_is_protocol', False)):
+ break
+ else:
+ if (not attr.startswith('_abc_') and
+ attr != '__abstractmethods__' and
+ attr != '_is_protocol' and
+ attr != '__dict__' and
+ attr != '_get_protocol_attrs' and
+ attr != '__module__'):
+ attrs.add(attr)
+
+ return attrs
+
+
+class AbstractGeneric(metaclass=AbstractGenericMeta):
+ """Base class for abstract generic classes."""
+
+
+class TypeAlias:
+ """Class for defining generic aliases for library types."""
+
+ def __init__(self, target_type):
+ self.target_type = target_type
+
+ def __getitem__(self, typeargs):
+ return self.target_type
+
+
+Traceback = object() # TODO proper type object
+
+
+# Define aliases for built-in types that support indexing.
+List = TypeAlias(list)
+Dict = TypeAlias(dict)
+Set = TypeAlias(set)
+Tuple = TypeAlias(tuple)
+Function = TypeAlias(callable)
+Pattern = TypeAlias(type(re.compile('')))
+Match = TypeAlias(type(re.match('', '')))
+
+def union(x): return x
+
+Union = TypeAlias(union)
+
+class typevar:
+ def __init__(self, name, *, values=None):
+ self.name = name
+ self.values = values
+
+
+# Predefined type variables.
+AnyStr = typevar('AnyStr', values=(str, bytes))
+
+
+class forwardref:
+ def __init__(self, name):
+ self.name = name
+
+
+def Any(x):
+ """The Any type; can also be used to cast a value to type Any."""
+ return x
+
+def cast(type, object):
+ """Cast a value to a type.
+
+ This only affects static checking; simply return object at runtime.
+ """
+ return object
+
+
+def overload(func):
+ """Function decorator for defining overloaded functions."""
+ frame = sys._getframe(1)
+ locals = frame.f_locals
+ # See if there is a previous overload variant available. Also verify
+ # that the existing function really is overloaded: otherwise, replace
+ # the definition. The latter is actually important if we want to reload
+ # a library module such as genericpath with a custom one that uses
+ # overloading in the implementation.
+ if func.__name__ in locals and hasattr(locals[func.__name__], 'dispatch'):
+ orig_func = locals[func.__name__]
+
+ def wrapper(*args, **kwargs):
+ ret, ok = orig_func.dispatch(*args, **kwargs)
+ if ok:
+ return ret
+ return func(*args, **kwargs)
+ wrapper.isoverload = True
+ wrapper.dispatch = make_dispatcher(func, orig_func.dispatch)
+ wrapper.next = orig_func
+ wrapper.__name__ = func.__name__
+ if hasattr(func, '__isabstractmethod__'):
+ # Note that we can't reliably check that abstractmethod is
+ # used consistently across overload variants, so we let a
+ # static checker do it.
+ wrapper.__isabstractmethod__ = func.__isabstractmethod__
+ return wrapper
+ else:
+ # Return the initial overload variant.
+ func.isoverload = True
+ func.dispatch = make_dispatcher(func)
+ func.next = None
+ return func
+
+
+def is_erased_type(t):
+ return t is Any or isinstance(t, typevar)
+
+
+def make_dispatcher(func, previous=None):
+ """Create argument dispatcher for an overloaded function.
+
+ Also handle chaining of multiple overload variants.
+ """
+ (args, varargs, varkw, defaults,
+ kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(func)
+
+ argtypes = []
+ for arg in args:
+ ann = annotations.get(arg)
+ if isinstance(ann, forwardref):
+ ann = ann.name
+ if is_erased_type(ann):
+ ann = None
+ elif isinstance(ann, str):
+ # The annotation is a string => evaluate it lazily when the
+ # overloaded function is first called.
+ frame = sys._getframe(2)
+ t = [None]
+ ann_str = ann
+ def check(x):
+ if not t[0]:
+ # Evaluate string in the context of the overload caller.
+ t[0] = eval(ann_str, frame.f_globals, frame.f_locals)
+ if is_erased_type(t[0]):
+ # Anything goes.
+ t[0] = object
+ if isinstance(t[0], type):
+ return isinstance(x, t[0])
+ else:
+ return t[0](x)
+ ann = check
+ argtypes.append(ann)
+
+ maxargs = len(argtypes)
+ minargs = maxargs
+ if defaults:
+ minargs = len(argtypes) - len(defaults)
+
+ def dispatch(*args, **kwargs):
+ if previous:
+ ret, ok = previous(*args, **kwargs)
+ if ok:
+ return ret, ok
+
+ nargs = len(args)
+ if nargs < minargs or nargs > maxargs:
+ # Invalid argument count.
+ return None, False
+
+ for i in range(nargs):
+ argtype = argtypes[i]
+ if argtype:
+ if isinstance(argtype, type):
+ if not isinstance(args[i], argtype):
+ break
+ else:
+ if not argtype(args[i]):
+ break
+ else:
+ return func(*args, **kwargs), True
+ return None, False
+ return dispatch
+
+
+class Undefined:
+ """Class that represents an undefined value with a specified type.
+
+ At runtime the name Undefined is bound to an instance of this
+ class. The intent is that any operation on an Undefined object
+ raises an exception, including use in a boolean context. Some
+ operations cannot be disallowed: Undefined can be used as an
+ operand of 'is', and it can be assigned to variables and stored in
+ containers.
+
+ 'Undefined' makes it possible to declare the static type of a
+ variable even if there is no useful default value to initialize it
+ with:
+
+ from typing import Undefined
+ x = Undefined(int)
+ y = Undefined # type: int
+
+ The latter form can be used if efficiency is of utmost importance,
+ since it saves a call operation and potentially additional
+ operations needed to evaluate a type expression. Undefined(x)
+ just evaluates to Undefined, ignoring the argument value.
+ """
+
+ def __repr__(self):
+ return ''
+
+ def __setattr__(self, attr, value):
+ raise AttributeError("'Undefined' object has no attribute '%s'" % attr)
+
+ def __eq__(self, other):
+ raise TypeError("'Undefined' object cannot be compared")
+
+ def __call__(self, type):
+ return self
+
+ def __bool__(self):
+ raise TypeError("'Undefined' object is not valid as a boolean")
+
+
+Undefined = Undefined()
+
+
+# Abstract classes
+
+
+T = typevar('T')
+KT = typevar('KT')
+VT = typevar('VT')
+
+
+class SupportsInt(Protocol):
+ @abstractmethod
+ def __int__(self) -> int: pass
+
+
+class SupportsFloat(Protocol):
+ @abstractmethod
+ def __float__(self) -> float: pass
+
+
+class SupportsAbs(Protocol[T]):
+ @abstractmethod
+ def __abs__(self) -> T: pass
+
+
+class SupportsRound(Protocol[T]):
+ @abstractmethod
+ def __round__(self, ndigits: int = 0) -> T: pass
+
+
+class Reversible(Protocol[T]):
+ @abstractmethod
+ def __reversed__(self) -> 'Iterator[T]': pass
+
+
+class Sized(Protocol):
+ @abstractmethod
+ def __len__(self) -> int: pass
+
+
+class Container(Protocol[T]):
+ @abstractmethod
+ def __contains__(self, x) -> bool: pass
+
+
+class Iterable(Protocol[T]):
+ @abstractmethod
+ def __iter__(self) -> 'Iterator[T]': pass
+
+
+class Iterator(Iterable[T], Protocol[T]):
+ @abstractmethod
+ def __next__(self) -> T: pass
+
+
+class Sequence(Sized, Iterable[T], Container[T], AbstractGeneric[T]):
+ @abstractmethod
+ @overload
+ def __getitem__(self, i: int) -> T: pass
+
+ @abstractmethod
+ @overload
+ def __getitem__(self, s: slice) -> 'Sequence[T]': pass
+
+ @abstractmethod
+ def __reversed__(self, s: slice) -> Iterator[T]: pass
+
+ @abstractmethod
+ def index(self, x) -> int: pass
+
+ @abstractmethod
+ def count(self, x) -> int: pass
+
+
+for t in list, tuple, str, bytes, range:
+ Sequence.register(t)
+
+
+class AbstractSet(Sized, Iterable[T], AbstractGeneric[T]):
+ @abstractmethod
+ def __contains__(self, x: object) -> bool: pass
+ @abstractmethod
+ def __and__(self, s: 'AbstractSet[T]') -> 'AbstractSet[T]': pass
+ @abstractmethod
+ def __or__(self, s: 'AbstractSet[T]') -> 'AbstractSet[T]': pass
+ @abstractmethod
+ def __sub__(self, s: 'AbstractSet[T]') -> 'AbstractSet[T]': pass
+ @abstractmethod
+ def __xor__(self, s: 'AbstractSet[T]') -> 'AbstractSet[T]': pass
+ @abstractmethod
+ def isdisjoint(self, s: 'AbstractSet[T]') -> bool: pass
+
+
+for t in set, frozenset, type({}.keys()), type({}.items()):
+ AbstractSet.register(t)
+
+
+class Mapping(Sized, Iterable[KT], AbstractGeneric[KT, VT]):
+ @abstractmethod
+ def __getitem__(self, k: KT) -> VT: pass
+ @abstractmethod
+ def __setitem__(self, k: KT, v: VT) -> None: pass
+ @abstractmethod
+ def __delitem__(self, v: KT) -> None: pass
+ @abstractmethod
+ def __contains__(self, o: object) -> bool: pass
+
+ @abstractmethod
+ def clear(self) -> None: pass
+ @abstractmethod
+ def copy(self) -> 'Mapping[KT, VT]': pass
+ @overload
+ @abstractmethod
+ def get(self, k: KT) -> VT: pass
+ @overload
+ @abstractmethod
+ def get(self, k: KT, default: VT) -> VT: pass
+ @overload
+ @abstractmethod
+ def pop(self, k: KT) -> VT: pass
+ @overload
+ @abstractmethod
+ def pop(self, k: KT, default: VT) -> VT: pass
+ @abstractmethod
+ def popitem(self) -> Tuple[KT, VT]: pass
+ @overload
+ @abstractmethod
+ def setdefault(self, k: KT) -> VT: pass
+ @overload
+ @abstractmethod
+ def setdefault(self, k: KT, default: VT) -> VT: pass
+
+ @overload
+ @abstractmethod
+ def update(self, m: 'Mapping[KT, VT]') -> None: pass
+ @overload
+ @abstractmethod
+ def update(self, m: Iterable[Tuple[KT, VT]]) -> None: pass
+
+ @abstractmethod
+ def keys(self) -> AbstractSet[KT]: pass
+ @abstractmethod
+ def values(self) -> AbstractSet[VT]: pass
+ @abstractmethod
+ def items(self) -> AbstractSet[Tuple[KT, VT]]: pass
+
+
+# TODO Consider more types: os.environ, etc. However, these add dependencies.
+Mapping.register(dict)
+
+
+# Note that the BinaryIO and TextIO classes must be in sync with typing module
+# stubs.
+
+
+class IO(AbstractGeneric[AnyStr]):
+ @abstractproperty
+ def mode(self) -> str: pass
+ @abstractproperty
+ def name(self) -> str: pass
+ @abstractmethod
+ def close(self) -> None: pass
+ @abstractmethod
+ def closed(self) -> bool: pass
+ @abstractmethod
+ def fileno(self) -> int: pass
+ @abstractmethod
+ def flush(self) -> None: pass
+ @abstractmethod
+ def isatty(self) -> bool: pass
+ @abstractmethod
+ def read(self, n: int = -1) -> AnyStr: pass
+ @abstractmethod
+ def readable(self) -> bool: pass
+ @abstractmethod
+ def readline(self, limit: int = -1) -> AnyStr: pass
+ @abstractmethod
+ def readlines(self, hint: int = -1) -> List[AnyStr]: pass
+ @abstractmethod
+ def seek(self, offset: int, whence: int = 0) -> int: pass
+ @abstractmethod
+ def seekable(self) -> bool: pass
+ @abstractmethod
+ def tell(self) -> int: pass
+ @abstractmethod
+ def truncate(self, size: int = None) -> int: pass
+ @abstractmethod
+ def writable(self) -> bool: pass
+ @abstractmethod
+ def write(self, s: AnyStr) -> int: pass
+ @abstractmethod
+ def writelines(self, lines: List[AnyStr]) -> None: pass
+
+ @abstractmethod
+ def __enter__(self) -> 'IO[AnyStr]': pass
+ @abstractmethod
+ def __exit__(self, type, value, traceback) -> None: pass
+
+
+class BinaryIO(IO[bytes]):
+ @overload
+ @abstractmethod
+ def write(self, s: bytes) -> int: pass
+ @overload
+ @abstractmethod
+ def write(self, s: bytearray) -> int: pass
+
+ @abstractmethod
+ def __enter__(self) -> 'BinaryIO': pass
+
+
+class TextIO(IO[str]):
+ @abstractproperty
+ def buffer(self) -> BinaryIO: pass
+ @abstractproperty
+ def encoding(self) -> str: pass
+ @abstractproperty
+ def errors(self) -> str: pass
+ @abstractproperty
+ def line_buffering(self) -> bool: pass
+ @abstractproperty
+ def newlines(self) -> Any: pass
+ @abstractmethod
+ def __enter__(self) -> 'TextIO': pass
+
+
+# TODO Register IO/TextIO/BinaryIO as the base class of file-like types.
+
+
+del t
diff --git a/python/testSrc/com/jetbrains/python/PyTypingTest.java b/python/testSrc/com/jetbrains/python/PyTypingTest.java
index 7efc34498a32..ef8a0928dbd5 100644
--- a/python/testSrc/com/jetbrains/python/PyTypingTest.java
+++ b/python/testSrc/com/jetbrains/python/PyTypingTest.java
@@ -26,6 +26,8 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
+ * Tests for a type system based on mypy's typing module.
+ *
* @author vlan
*/
public class PyTypingTest extends PyTestCase {
@@ -81,13 +83,27 @@ public class PyTypingTest extends PyTestCase {
"expr = f()\n");
}
+ public void testUnionType() {
+ doTest("int | str",
+ "from typing import Union\n" +
+ "\n" +
+ "def f(expr: Union[int, str]):\n" +
+ " pass\n");
+ }
+
private void doTest(@NotNull String expectedType, @NotNull String text) {
myFixture.copyDirectoryToProject("typing", "");
myFixture.configureByText(PythonFileType.INSTANCE, text);
final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);
- final TypeEvalContext context = TypeEvalContext.userInitiated(expr.getContainingFile()).withTracing();
- PyType actual = context.getType(expr);
+ final TypeEvalContext codeAnalysis = TypeEvalContext.codeAnalysis(expr.getContainingFile());
+ final TypeEvalContext userInitiated = TypeEvalContext.userInitiated(expr.getContainingFile()).withTracing();
+ assertType(expectedType, expr, codeAnalysis, "code analysis");
+ assertType(expectedType, expr, userInitiated, "user initiated");
+ }
+
+ private static void assertType(String expectedType, PyExpression expr, TypeEvalContext context, String contextName) {
+ final PyType actual = context.getType(expr);
final String actualType = PythonDocumentationProvider.getTypeName(actual, context);
- assertEquals(expectedType, actualType);
+ assertEquals("Failed in " + contextName + " context", expectedType, actualType);
}
}