PY-80843 Migrate PyTypeModelBuilder to PyTypeVisitor

The existing behavior of type rendering was preserved as much as possible.
However, a few things were improved:
- type renderings are no longer cached, so e.g. union types are
formatted in the actual order of their component types
- `type` class type is rendered as type[type], similarly to other type checkers' behavior PY-80352
- Types of TypedDict classes are properly rendered as type[MyTypedDict]

GitOrigin-RevId: 94f9074a2f7a0a82a4e2de39ed049b50cec40aed
This commit is contained in:
Mikhail Golubev
2025-02-19 17:37:54 +02:00
committed by intellij-monorepo-bot
parent c34effd25c
commit a2d43e2f78
8 changed files with 495 additions and 902 deletions

View File

@@ -1,887 +0,0 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.documentation;
import com.google.common.collect.Collections2;
import com.google.common.collect.Maps;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.highlighting.PyHighlighter;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyExpression;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher;
import com.jetbrains.python.psi.types.*;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.jetbrains.python.documentation.PyDocSignaturesHighlighterKt.highlightExpressionText;
import static com.jetbrains.python.documentation.PyDocSignaturesHighlighterKt.styledSpan;
import static com.jetbrains.python.documentation.PyDocumentationLink.toClass;
import static com.jetbrains.python.psi.types.PyNoneTypeKt.isNoneType;
public class PyTypeModelBuilder {
private final Map<PyType, TypeModel> myVisited = Maps.newHashMap();
private final TypeEvalContext myContext;
PyTypeModelBuilder(TypeEvalContext context) {
this.myContext = context;
}
abstract static class TypeModel {
abstract void accept(@NotNull TypeVisitor visitor);
public @NotNull String asString() {
final TypeToStringVisitor visitor = new TypeToStringVisitor();
accept(visitor);
return visitor.getString();
}
public void toBodyWithLinks(@NotNull HtmlBuilder body, @NotNull PsiElement anchor) {
final TypeToBodyWithLinksVisitor visitor = new TypeToBodyWithLinksVisitor(body, anchor);
accept(visitor);
}
public @NotNull String asDescription() {
final TypeToDescriptionVisitor visitor = new TypeToDescriptionVisitor();
accept(visitor);
return visitor.getDescription();
}
public @NotNull String asPep484TypeHint() {
final TypeToStringVisitor visitor = new TypeToPep484TypeHintVisitor();
accept(visitor);
return visitor.getString();
}
public @NotNull String asStringWithAdditionalInfo() {
TypeToStringVisitor visitor = new VerboseTypeInfoVisitor();
accept(visitor);
return visitor.getString();
}
}
static final class OneOf extends TypeModel {
private final Collection<TypeModel> oneOfTypes;
private final boolean bitwiseOrUnionAllowed;
private OneOf(Collection<TypeModel> oneOfTypes, boolean bitwiseOrUnionAllowed) {
this.oneOfTypes = oneOfTypes;
this.bitwiseOrUnionAllowed = bitwiseOrUnionAllowed;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.oneOf(this);
}
}
private static final class NarrowedType extends TypeModel {
private final TypeModel narrowedType;
private final boolean isTypeIs;
private NarrowedType(@NotNull TypeModel narrowedType, boolean isTypeIs) {
this.narrowedType = narrowedType;
this.isTypeIs = isTypeIs;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.narrowedType(this);
}
}
private static final class CollectionOf extends TypeModel {
private final TypeModel collectionType;
private final List<TypeModel> elementTypes;
private final boolean useTypingAlias;
private CollectionOf(TypeModel collectionType,
List<TypeModel> elementTypes,
boolean useTypingAlias) {
this.collectionType = collectionType;
this.elementTypes = elementTypes;
this.useTypingAlias = useTypingAlias;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.collectionOf(this);
}
}
static final class NamedType extends TypeModel {
private static final @NotNull NamedType ANY = new NamedType(PyNames.UNKNOWN_TYPE);
private final @Nullable @NlsSafe String name;
private NamedType(@Nullable String name) {
this.name = name;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.name(name);
}
private static @NotNull NamedType nameOrAny(@Nullable PyType type) {
return type == null ? ANY : new NamedType(type.getName());
}
}
static final class UnknownType extends TypeModel {
private final TypeModel type;
private final boolean bitwiseOrUnionAllowed;
private UnknownType(TypeModel type, boolean bitwiseOrUnionAllowed) {
this.type = type;
this.bitwiseOrUnionAllowed = bitwiseOrUnionAllowed;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.unknown(this);
}
}
static final class OptionalType extends TypeModel {
private final TypeModel type;
private final boolean bitwiseOrUnionAllowed;
private OptionalType(TypeModel type, boolean bitwiseOrUnionAllowed) {
this.type = type;
this.bitwiseOrUnionAllowed = bitwiseOrUnionAllowed;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.optional(this);
}
}
static class TupleType extends TypeModel {
private final List<TypeModel> members;
private final boolean homogeneous;
private final boolean useTypingAlias;
TupleType(List<TypeModel> members, boolean homogeneous, boolean useTypingAlias) {
this.members = members;
this.homogeneous = homogeneous;
this.useTypingAlias = useTypingAlias;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.tuple(this);
}
}
static final class FunctionType extends TypeModel {
private final @NotNull TypeModel returnType;
private final @Nullable Collection<TypeModel> parameters;
private FunctionType(@Nullable TypeModel returnType, @Nullable Collection<TypeModel> parameters) {
this.returnType = returnType != null ? returnType : NamedType.ANY;
this.parameters = parameters;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.function(this);
}
}
static final class ParamType extends TypeModel {
private final @Nullable @NlsSafe String name;
private final @Nullable TypeModel type;
private ParamType(@Nullable String name, @Nullable TypeModel type) {
this.name = name;
this.type = type;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.param(this);
}
}
static class ClassObjectType extends TypeModel {
private final TypeModel classType;
private final boolean useTypingAlias;
ClassObjectType(TypeModel classType, boolean useTypingAlias) {
this.classType = classType;
this.useTypingAlias = useTypingAlias;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.classObject(this);
}
}
static class TypeVarType extends TypeModel {
private final @NlsSafe String name;
private final TypeModel bound;
TypeVarType(@Nullable String name, @Nullable TypeModel bound) {
this.name = name;
this.bound = bound;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.typeVarType(this);
}
}
private static final class OneOfLiterals extends TypeModel {
private final @NotNull List<PyLiteralType> literals;
private OneOfLiterals(@NotNull List<PyLiteralType> literals) {
this.literals = literals;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.oneOfLiterals(this);
}
}
private static class CallableParameterList extends TypeModel {
private final List<TypeModel> parameters;
private CallableParameterList(@NotNull List<TypeModel> parameters) {
this.parameters = parameters;
}
@Override
void accept(@NotNull TypeVisitor visitor) {
visitor.callableParameterList(this);
}
}
/**
* Builds tree-like type model for PyType
*/
@SuppressWarnings("rawtypes")
public TypeModel build(@Nullable PyType type,
boolean allowUnions) {
final TypeModel evaluated = myVisited.get(type);
if (evaluated != null) {
return evaluated;
}
if (myVisited.containsKey(type)) { //already evaluating?
return NamedType.nameOrAny(type);
}
myVisited.put(type, null); //mark as evaluating
TypeModel result = null;
if (type instanceof PyTypedDictType) {
result = NamedType.nameOrAny(type);
}
else if (type instanceof PyInstantiableType && ((PyInstantiableType<?>)type).isDefinition()) {
final PyInstantiableType instanceType = ((PyInstantiableType<?>)type).toInstance();
// Special case: render Type[type] as just type
if (type instanceof PyClassType && instanceType.equals(PyBuiltinCache.getInstance(((PyClassType)type).getPyClass()).getTypeType())) {
result = NamedType.nameOrAny(type);
}
else {
boolean useTypingAlias = PythonLanguageLevelPusher
.getLanguageLevelForFile(myContext.getOrigin().getOriginalFile())
.isOlderThan(LanguageLevel.PYTHON39);
result = new ClassObjectType(build(instanceType, allowUnions), useTypingAlias);
}
}
else if (type instanceof PyNamedTupleType) {
result = NamedType.nameOrAny(type);
}
else if (type instanceof PyTupleType tupleType) {
final List<PyType> elementTypes = tupleType.isHomogeneous()
? Collections.singletonList(tupleType.getIteratedItemType())
: tupleType.getElementTypes();
boolean useTypingAlias =
PythonLanguageLevelPusher.getLanguageLevelForFile(tupleType.getPyClass().getContainingFile()).isOlderThan(LanguageLevel.PYTHON39);
final List<TypeModel> elementModels = ContainerUtil.map(elementTypes, elementType -> build(elementType, true));
result = new TupleType(elementModels, tupleType.isHomogeneous(), useTypingAlias);
}
else if (type instanceof PyCollectionType asCollection) {
final List<TypeModel> elementModels = new ArrayList<>();
for (PyType elementType : asCollection.getElementTypes()) {
elementModels.add(build(elementType, true));
}
if (!elementModels.isEmpty()) {
PyClass pyClass = asCollection.getPyClass();
final TypeModel collectionType = build(new PyClassTypeImpl(pyClass, asCollection.isDefinition()), false);
boolean useTypingAlias =
PythonLanguageLevelPusher.getLanguageLevelForFile(pyClass.getContainingFile()).isOlderThan(LanguageLevel.PYTHON39);
result = new CollectionOf(collectionType, elementModels, useTypingAlias);
}
}
else if (type instanceof PyNarrowedType narrowedType) {
result = new NarrowedType(build(narrowedType.getNarrowedType(), true), narrowedType.getTypeIs());
}
else if (type instanceof PyUnionType unionType && allowUnions) {
final Collection<PyType> unionMembers = unionType.getMembers();
final Pair<List<PyLiteralType>, List<PyType>> literalsAndOthers = extractLiterals(unionType);
final Ref<PyType> optionalType = getOptionalType(unionType);
if (literalsAndOthers != null) {
final OneOfLiterals oneOfLiterals = new OneOfLiterals(literalsAndOthers.first);
if (!literalsAndOthers.second.isEmpty()) {
final List<TypeModel> otherTypeModels = ContainerUtil.map(literalsAndOthers.second, t -> build(t, false));
result = new OneOf(ContainerUtil.prepend(otherTypeModels, oneOfLiterals),
PyTypingTypeProvider.isBitwiseOrUnionAvailable(myContext));
}
else {
result = oneOfLiterals;
}
}
else if (optionalType != null) {
result = new OptionalType(build(optionalType.get(), true), PyTypingTypeProvider.isBitwiseOrUnionAvailable(myContext));
}
else if (type instanceof PyDynamicallyEvaluatedType || PyTypeChecker.isUnknown(type, false, myContext)) {
result = new UnknownType(build(unionType.excludeNull(), true), PyTypingTypeProvider.isBitwiseOrUnionAvailable(myContext));
}
else if (ContainerUtil.all(unionMembers, t -> t instanceof PyClassType && ((PyClassType)t).isDefinition())) {
final List<TypeModel> instanceTypes = ContainerUtil.map(unionMembers, t -> build(((PyClassType)t).toInstance(), allowUnions));
final var useTypingAlias = LanguageLevel.forElement(myContext.getOrigin().getOriginalFile()).isOlderThan(LanguageLevel.PYTHON39);
result = new ClassObjectType(new OneOf(instanceTypes, PyTypingTypeProvider.isBitwiseOrUnionAvailable(myContext)), useTypingAlias);
}
else {
result = new OneOf(Collections2.transform(unionMembers, t -> build(t, false)),
PyTypingTypeProvider.isBitwiseOrUnionAvailable(myContext));
}
}
else if (type instanceof PyCallableType && !(type instanceof PyClassLikeType)) {
result = buildCallable((PyCallableType)type);
}
else if (type instanceof PyTypeVarType typeVarType) {
PyType effectiveBound = PyTypeUtil.getEffectiveBound(typeVarType);
result = new TypeVarType(type.getName(), effectiveBound != null ? build(effectiveBound, true) : null);
}
else if (type instanceof PyCallableParameterListType callableParameterListType) {
result = new CallableParameterList(buildParameterModels(callableParameterListType.getParameters()));
}
if (result == null) {
result = NamedType.nameOrAny(type);
}
myVisited.put(type, result);
return result;
}
private static @Nullable Ref<PyType> getOptionalType(@NotNull PyUnionType type) {
final Collection<PyType> members = type.getMembers();
if (members.size() == 2) {
boolean foundNone = false;
PyType optional = null;
for (PyType member : members) {
if (isNoneType(member)) {
foundNone = true;
}
else if (member != null) {
optional = member;
}
}
if (foundNone) {
return Ref.create(optional);
}
}
return null;
}
private static @Nullable Pair<@NotNull List<PyLiteralType>, @NotNull List<PyType>> extractLiterals(@NotNull PyUnionType type) {
final Collection<PyType> members = type.getMembers();
final List<PyLiteralType> literalTypes = ContainerUtil.filterIsInstance(members, PyLiteralType.class);
if (literalTypes.size() < 2) return null;
final List<PyType> otherTypes = ContainerUtil.filter(members, m -> !(m instanceof PyLiteralType));
return Pair.create(literalTypes, otherTypes);
}
private TypeModel buildCallable(@NotNull PyCallableType type) {
final List<PyCallableParameter> parameters = type.getParameters(myContext);
List<TypeModel> parameterModels = null;
if (parameters != null) {
parameterModels = buildParameterModels(parameters);
}
final PyType ret = type.getReturnType(myContext);
final TypeModel returnType = build(ret, true);
return new FunctionType(returnType, parameterModels);
}
private @NotNull List<TypeModel> buildParameterModels(@NotNull List<PyCallableParameter> parameters) {
List<TypeModel> parameterModels = new ArrayList<>();
for (PyCallableParameter parameter : parameters) {
final var paramType = parameter.getType(myContext);
if (paramType instanceof PyParamSpecType || paramType instanceof PyConcatenateType) {
parameterModels.add(new ParamType(null, build(parameter.getType(myContext), true)));
}
else {
parameterModels.add(new ParamType(parameter.getName(), build(parameter.getType(myContext), true)));
}
}
return parameterModels;
}
private interface TypeVisitor {
void oneOf(OneOf oneOf);
void collectionOf(CollectionOf collectionOf);
void name(String name);
void function(FunctionType type);
void param(ParamType text);
void unknown(UnknownType type);
void optional(OptionalType type);
void tuple(TupleType type);
void classObject(ClassObjectType type);
void typeVarType(TypeVarType type);
void oneOfLiterals(OneOfLiterals literals);
void narrowedType(NarrowedType type);
void callableParameterList(CallableParameterList list);
}
private static class TypeToStringVisitor extends TypeNameVisitor {
@Override
protected @NotNull HtmlChunk styled(@Nls String text, @NotNull TextAttributesKey style) {
return HtmlChunk.raw(StringUtil.notNullize(text));
}
@Override
protected @NotNull HtmlChunk escaped(@Nls String text) {
return HtmlChunk.raw(StringUtil.notNullize(text));
}
@Override
protected @NotNull HtmlChunk className(@Nls String name) {
return escaped(name);
}
@Override
protected @NotNull HtmlChunk styledExpression(@Nls String expressionText, @NotNull PyExpression expression) {
return HtmlChunk.raw(StringUtil.notNullize(expressionText));
}
@Override
protected @NotNull HtmlChunk toClass(@NotNull String qualifiedName, @Nls @NotNull String linkText, @Nullable TextAttributesKey style) {
return HtmlChunk.raw(linkText);
}
public String getString() {
return myBody.toString();
}
@Override
public void unknown(UnknownType type) {
final TypeModel nested = type.type;
if (nested != null) {
if (type.bitwiseOrUnionAllowed) {
nested.accept(this);
add(HtmlChunk.raw(" | " + PyNames.UNKNOWN_TYPE));
}
else {
add(HtmlChunk.raw("Union[")); // NON-NLS
nested.accept(this);
add(HtmlChunk.raw(", " + PyNames.UNKNOWN_TYPE + "]"));
}
}
}
}
private static class TypeToPep484TypeHintVisitor extends TypeToStringVisitor {
@Override
protected boolean maxDepthExceeded() {
return false;
}
@Override
public void function(FunctionType function) {
add(HtmlChunk.raw("Callable[")); //NON-NLS
final Collection<TypeModel> parameters = function.parameters;
if (parameters != null) {
add(HtmlChunk.raw("["));
processList(parameters);
add(HtmlChunk.raw("]"));
}
else {
add(HtmlChunk.raw("..."));
}
add(HtmlChunk.raw(", "));
function.returnType.accept(this);
add(HtmlChunk.raw("]"));
}
@Override
public void param(ParamType param) {
if (param.type != null) {
param.type.accept(this);
}
else {
add(HtmlChunk.raw(PyNames.UNKNOWN_TYPE));
}
}
@Override
public void collectionOf(CollectionOf collectionOf) {
typingGenericFormat(collectionOf);
}
}
private static class TypeToBodyWithLinksVisitor extends TypeNameVisitor {
private final PsiElement myAnchor;
TypeToBodyWithLinksVisitor(HtmlBuilder body, PsiElement anchor) {
myBody = body;
myAnchor = anchor;
}
@Override
protected @NotNull HtmlChunk styled(@Nls String text, @NotNull TextAttributesKey style) {
return styledSpan(text, style);
}
@Override
protected @NotNull HtmlChunk escaped(@Nls String text) {
return HtmlChunk.text(text);
}
@Override
protected @NotNull HtmlChunk className(@Nls String name) {
final TypeEvalContext context = TypeEvalContext.userInitiated(myAnchor.getProject(), myAnchor.getContainingFile());
return PyDocumentationLink.toPossibleClass(name, myAnchor, context);
}
@Override
protected @NotNull HtmlChunk styledExpression(@Nls String expressionText, @NotNull PyExpression expression) {
return highlightExpressionText(expressionText, expression);
}
@Override
protected @NotNull HtmlChunk toClass(@NotNull String qualifiedName, @Nls @NotNull String linkText, @Nullable TextAttributesKey style) {
final var result = PyDocumentationLink.toClass(qualifiedName, linkText);
return style != null ? styledSpan(result, style) : result;
}
}
private static class TypeToDescriptionVisitor extends TypeNameVisitor {
@Override
protected @NotNull HtmlChunk styled(@Nls String text, @NotNull TextAttributesKey style) {
return HtmlChunk.raw(StringUtil.notNullize(text));
}
@Override
protected @NotNull HtmlChunk escaped(@Nls String text) {
return HtmlChunk.raw(StringUtil.notNullize(text));
}
@Override
protected @NotNull HtmlChunk className(@Nls String name) {
return escaped(name);
}
@Override
protected @NotNull HtmlChunk styledExpression(@Nls String expressionText, @NotNull PyExpression expression) {
return HtmlChunk.raw(StringUtil.notNullize(expressionText));
}
@Override
protected @NotNull HtmlChunk toClass(@NotNull String qualifiedName, @Nls @NotNull String linkText, @Nullable TextAttributesKey style) {
return HtmlChunk.raw(linkText);
}
public @NotNull String getDescription() {
return myBody.toString();
}
}
private abstract static class TypeNameVisitor implements TypeVisitor {
private int myDepth = 0;
private static final int MAX_DEPTH = 6;
private boolean switchBuiltinToTyping = false;
protected HtmlBuilder myBody = new HtmlBuilder();
@Override
public void oneOf(OneOf oneOf) {
myDepth++;
if (maxDepthExceeded()) {
add(styled("...", PyHighlighter.PY_DOT));
return;
}
if (oneOf.bitwiseOrUnionAllowed) {
processList(oneOf.oneOfTypes, " | ");
}
else {
add(escaped("Union")); //NON-NLS
add(styled("[", PyHighlighter.PY_BRACKETS));
processList(oneOf.oneOfTypes);
add(styled("]", PyHighlighter.PY_BRACKETS));
}
myDepth--;
}
protected void processList(@NotNull Collection<TypeModel> list) {
processList(list, ", ");
}
protected void processList(@NotNull Collection<TypeModel> list, @NotNull @Nls String separator) {
boolean first = true;
for (TypeModel t : list) {
if (!first) {
if (separator.equals(", ")) {
add(styled(separator, PyHighlighter.PY_COMMA));
}
else if (separator.equals(" | ")) {
add(styled(separator, PyHighlighter.PY_OPERATION_SIGN));
}
else {
add(escaped(separator));
}
}
else {
first = false;
}
t.accept(this);
}
}
protected void add(@NotNull HtmlChunk chunk) {
myBody.append(chunk);
}
protected abstract @NotNull HtmlChunk styled(@Nls String text, @NotNull TextAttributesKey style);
protected abstract @NotNull HtmlChunk escaped(@Nls String text);
protected abstract @NotNull HtmlChunk className(@Nls String name);
protected abstract @NotNull HtmlChunk styledExpression(@Nls String expressionText, @NotNull PyExpression expression);
protected abstract @NotNull HtmlChunk toClass(@NotNull String qualifiedName, @NotNull @Nls String linkText, @Nullable TextAttributesKey style);
@Override
public void collectionOf(CollectionOf collectionOf) {
myDepth++;
if (maxDepthExceeded()) {
add(styled("...", PyHighlighter.PY_DOT));
return;
}
final boolean allTypeParamsAreAny = ContainerUtil.and(collectionOf.elementTypes, t -> t == NamedType.ANY);
if (allTypeParamsAreAny) {
collectionOf.collectionType.accept(this);
}
else {
typingGenericFormat(collectionOf);
}
myDepth--;
}
protected void typingGenericFormat(CollectionOf collectionOf) {
final boolean prevSwitchBuiltinToTyping = switchBuiltinToTyping;
switchBuiltinToTyping = collectionOf.useTypingAlias;
collectionOf.collectionType.accept(this);
switchBuiltinToTyping = prevSwitchBuiltinToTyping;
if (!collectionOf.elementTypes.isEmpty()) {
add(styled("[", PyHighlighter.PY_BRACKETS));
processList(collectionOf.elementTypes);
add(styled("]", PyHighlighter.PY_BRACKETS));
}
}
@Override
public void narrowedType(@NotNull NarrowedType narrowedType) {
if (narrowedType.isTypeIs) {
add(styled("TypeIs", PyHighlighter.PY_CLASS_DEFINITION));
}
else {
add(styled("TypeGuard", PyHighlighter.PY_CLASS_DEFINITION));
}
add(styled("[", PyHighlighter.PY_BRACKETS));
narrowedType.narrowedType.accept(this);
add(styled("]", PyHighlighter.PY_BRACKETS));
}
@Override
public void name(@NlsSafe String name) {
add(className(switchBuiltinToTyping ? PyTypingTypeProvider.TYPING_COLLECTION_CLASSES.getOrDefault(name, name) : name));
}
@Override
public void function(FunctionType function) {
myDepth++;
if (maxDepthExceeded()) {
add(styled("...", PyHighlighter.PY_DOT));
return;
}
add(styled("(", PyHighlighter.PY_PARENTHS));
final Collection<TypeModel> parameters = function.parameters;
if (parameters != null) {
processList(parameters);
}
else {
add(styled("...", PyHighlighter.PY_DOT));
}
add(styled(")", PyHighlighter.PY_PARENTHS));
add(escaped(" -> "));
function.returnType.accept(this);
myDepth--;
}
protected boolean maxDepthExceeded() {
return myDepth > MAX_DEPTH;
}
@Override
public void param(ParamType param) {
myDepth++;
if (maxDepthExceeded()) {
add(styled("...", PyHighlighter.PY_DOT));
return;
}
if (param.name != null) {
add(styled(param.name, PyHighlighter.PY_PARAMETER));
}
if (param.type != null) {
if (param.name != null) {
add(styled(": ", PyHighlighter.PY_OPERATION_SIGN));
}
param.type.accept(this);
}
myDepth--;
}
@Override
public void unknown(UnknownType type) {
type.type.accept(this);
}
@Override
public void optional(OptionalType type) {
if (type.bitwiseOrUnionAllowed) {
type.type.accept(this);
add(styled(" | ", PyHighlighter.PY_OPERATION_SIGN));
add(toClass(PyNames.TYPE_NONE, PyNames.NONE, PyHighlighter.PY_KEYWORD)); //NON-NLS
}
else {
add(escaped("Optional")); //NON-NLS
add(styled("[", PyHighlighter.PY_BRACKETS));
type.type.accept(this);
add(styled("]", PyHighlighter.PY_BRACKETS));
}
}
@Override
public void tuple(TupleType type) {
if (type.useTypingAlias) {
add(escaped("Tuple")); //NON-NLS
}
else {
add(styled("tuple", PyHighlighter.PY_BUILTIN_NAME)); //NON-NLS
}
add(styled("[", PyHighlighter.PY_BRACKETS));
if (!type.members.isEmpty()) {
processList(type.members);
if (type.homogeneous) {
add(styled(", ", PyHighlighter.PY_COMMA));
add(styled("...", PyHighlighter.PY_DOT));
}
}
else {
add(styled("()", PyHighlighter.PY_PARENTHS));
}
add(styled("]", PyHighlighter.PY_BRACKETS));
}
@Override
public void classObject(ClassObjectType type) {
if (type.useTypingAlias) {
add(escaped("Type")); //NON-NLS
}
else {
add(styled("type", PyHighlighter.PY_BUILTIN_NAME)); //NON-NLS
}
add(styled("[", PyHighlighter.PY_BRACKETS));
type.classType.accept(this);
add(styled("]", PyHighlighter.PY_BRACKETS));
}
@Override
public void typeVarType(TypeVarType type) {
if (type.name != null) {
add(escaped(type.name));
}
}
@Override
public void oneOfLiterals(OneOfLiterals literals) {
add(
new HtmlBuilder()
.append(escaped("Literal")) //NON-NLS
.append(styled("[", PyHighlighter.PY_BRACKETS))
.append(StreamEx
.of(literals.literals)
.map(PyLiteralType::getExpression)
.map(expr -> styledExpression(expr.getText(), expr))
.collect(HtmlChunk.toFragment(styled(", ", PyHighlighter.PY_COMMA))))
.append(styled("]", PyHighlighter.PY_BRACKETS))
.toFragment());
}
@Override
public void callableParameterList(CallableParameterList list) {
add(HtmlChunk.raw("["));
processList(list.parameters);
add(HtmlChunk.raw("]"));
}
}
private static class VerboseTypeInfoVisitor extends TypeToStringVisitor {
@Override
public void typeVarType(TypeVarType type) {
if (type.name != null) {
add(escaped(type.name));
if (type.bound != null) {
add(escaped(" ≤: "));
type.bound.accept(this);
}
}
}
}
}

View File

@@ -0,0 +1,472 @@
package com.jetbrains.python.documentation;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.highlighting.PyHighlighter;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.psi.PyExpression;
import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher;
import com.jetbrains.python.psi.types.*;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import static com.jetbrains.python.documentation.PyDocSignaturesHighlighterKt.highlightExpressionText;
import static com.jetbrains.python.documentation.PyDocSignaturesHighlighterKt.styledSpan;
import static com.jetbrains.python.psi.types.PyNoneTypeKt.isNoneType;
// TODO visitPyConcatenateType
public abstract class PyTypeRenderer extends PyTypeVisitorExt<@NotNull HtmlChunk> {
private static final int MAX_DEPTH = 6;
protected int myDepth = 0;
protected final @NotNull TypeEvalContext myTypeEvalContext;
private PyTypeRenderer(@NotNull TypeEvalContext typeEvalContext) {
myTypeEvalContext = typeEvalContext;
}
private static abstract class HtmlRenderer extends PyTypeRenderer {
private final @NotNull PsiElement myAnchor;
private HtmlRenderer(@NotNull TypeEvalContext typeEvalContext, @NotNull PsiElement anchor) {
super(typeEvalContext);
myAnchor = anchor;
}
@Override
protected @NotNull HtmlChunk styled(@Nls String text, @NotNull TextAttributesKey style) {
return styledSpan(text, style);
}
@Override
protected @NotNull HtmlChunk escaped(@Nls String text) {
return HtmlChunk.text(text);
}
@Override
protected @NotNull HtmlChunk className(@Nls String name) {
return PyDocumentationLink.toPossibleClass(name, myAnchor, myTypeEvalContext);
}
@Override
protected @NotNull HtmlChunk toClass(@NotNull String qualifiedName, @Nls @NotNull String linkText, @Nullable TextAttributesKey style) {
final var result = PyDocumentationLink.toClass(qualifiedName, linkText);
return style != null ? styledSpan(result, style) : result;
}
@Override
protected @NotNull HtmlChunk styledExpression(@NotNull PyExpression expression) {
return highlightExpressionText(expression.getText(), expression);
}
}
public static final class RichDocumentation extends HtmlRenderer {
public RichDocumentation(@NotNull TypeEvalContext typeEvalContext, @NotNull PsiElement anchor) {
super(typeEvalContext, anchor);
}
@Override
protected boolean hideWeakUnions() {
return true;
}
}
public static final class Documentation extends PyTypeRenderer {
private final boolean mySimplifyWeakUnions;
public Documentation(@NotNull TypeEvalContext typeEvalContext, boolean simplifyWeakUnions) {
super(typeEvalContext);
mySimplifyWeakUnions = simplifyWeakUnions;
}
@Override
protected boolean hideWeakUnions() {
return mySimplifyWeakUnions;
}
}
public static final class VerboseDocumentation extends PyTypeRenderer {
public VerboseDocumentation(@NotNull TypeEvalContext typeEvalContext) {
super(typeEvalContext);
}
@Override
public @NotNull HtmlChunk visitPyTypeVarType(@NotNull PyTypeVarType typeVarType) {
HtmlBuilder result = new HtmlBuilder();
typeVarType.getName();
result.append(escaped(typeVarType.getName()));
PyType effectiveBound = PyTypeUtil.getEffectiveBound(typeVarType);
if (effectiveBound != null) {
result.append(escaped(" ≤: "));
result.append(render(effectiveBound));
}
return typeVarType.isDefinition() ? wrapInTypingType(result.toFragment()) : result.toFragment();
}
}
public static final class TypeHint extends PyTypeRenderer {
public TypeHint(@NotNull TypeEvalContext typeEvalContext) {
super(typeEvalContext);
}
@Override
protected boolean maxDepthExceeded() {
return false;
}
@Override
protected boolean hideAllAnyTypeArguments() {
return false;
}
@Override
public HtmlChunk visitPyCallableType(@NotNull PyCallableType callableType) {
HtmlBuilder result = new HtmlBuilder();
result.append(HtmlChunk.raw("Callable")); //NON-NLS
result.append(styled("[", PyHighlighter.PY_BRACKETS));
result.append(styled("[", PyHighlighter.PY_BRACKETS));
List<PyCallableParameter> parameters = callableType.getParameters(myTypeEvalContext);
if (parameters != null) {
result.append(renderList(ContainerUtil.map(parameters, this::visitPyCallableParameter)));
}
else {
result.append(styled("...", PyHighlighter.PY_DOT));
}
result.append(styled("]", PyHighlighter.PY_BRACKETS));
result.append(HtmlChunk.raw(", "));
result.append(render(callableType.getReturnType(myTypeEvalContext)));
result.append(styled("]", PyHighlighter.PY_BRACKETS));
return result.toFragment();
}
@Override
protected @NotNull HtmlChunk visitPyCallableParameter(@NotNull PyCallableParameter param) {
return render(param.getType(myTypeEvalContext));
}
}
protected boolean maxDepthExceeded() {
return myDepth > MAX_DEPTH;
}
protected @NotNull HtmlChunk render(@Nullable PyType type) {
if (maxDepthExceeded()) {
return styled("...", PyHighlighter.PY_DOT);
}
myDepth++;
try {
return visit(type, this);
}
finally {
myDepth--;
}
}
protected @NotNull HtmlChunk styled(@Nls String text, @NotNull TextAttributesKey style) {
return HtmlChunk.raw(StringUtil.notNullize(text));
}
protected @NotNull HtmlChunk escaped(@Nls String text) {
return HtmlChunk.raw(StringUtil.notNullize(text));
}
protected @NotNull HtmlChunk className(@Nls String name) {
return escaped(name);
}
protected @NotNull HtmlChunk toClass(@NotNull String qualifiedName, @Nls @NotNull String linkText, @Nullable TextAttributesKey style) {
return HtmlChunk.raw(linkText);
}
protected @NotNull HtmlChunk styledExpression(@NotNull PyExpression expression) {
return HtmlChunk.raw(expression.getText());
}
protected final boolean isBitwiseOrUnionAvailable() {
return PyTypingTypeProvider.isBitwiseOrUnionAvailable(myTypeEvalContext);
}
protected final boolean isGenericBuiltinsAvailable() {
PsiFile origin = myTypeEvalContext.getOrigin();
return origin == null || PythonLanguageLevelPusher.getLanguageLevelForFile(origin).isAtLeast(LanguageLevel.PYTHON39);
}
protected boolean hideWeakUnions() {
return false;
}
protected boolean hideAllAnyTypeArguments() {
return true;
}
@Override
public HtmlChunk visitPyGenericType(@NotNull PyCollectionType collectionOf) {
HtmlChunk genericTypeRender = renderGenericType(collectionOf);
return collectionOf.isDefinition() ? wrapInTypingType(genericTypeRender) : genericTypeRender;
}
private @NotNull HtmlChunk renderGenericType(@NotNull PyCollectionType genericType) {
HtmlBuilder result = new HtmlBuilder();
// TODO get rid of that behavior
boolean allTypeParamsAreAny = ContainerUtil.and(genericType.getElementTypes(), t -> t == null);
boolean renderTypeArgumentList = !genericType.getElementTypes().isEmpty() &&
!(hideAllAnyTypeArguments() && allTypeParamsAreAny);
String className = genericType.getPyClass().getName();
if (renderTypeArgumentList && !isGenericBuiltinsAvailable() && PyTypingTypeProvider.TYPING_COLLECTION_CLASSES.containsKey(className)) {
result.append(className(PyTypingTypeProvider.TYPING_COLLECTION_CLASSES.get(className))); // NON-NLS
}
else {
result.append(className(className));
}
if (renderTypeArgumentList) {
result.append(styled("[", PyHighlighter.PY_BRACKETS));
result.append(renderList(ContainerUtil.map(genericType.getElementTypes(), this::render)));
result.append(styled("]", PyHighlighter.PY_BRACKETS));
}
return result.toFragment();
}
protected @NotNull HtmlChunk wrapInTypingType(@NotNull HtmlChunk instanceTypeRender) {
return new HtmlBuilder()
.append(isGenericBuiltinsAvailable() ? styled("type", PyHighlighter.PY_BUILTIN_NAME) : escaped("Type")) //NON-NLS
.append(styled("[", PyHighlighter.PY_BRACKETS))
.append(instanceTypeRender)
.append(styled("]", PyHighlighter.PY_BRACKETS))
.toFragment();
}
@Override
public @NotNull HtmlChunk visitPyClassLikeType(@NotNull PyClassLikeType classLikeType) {
HtmlChunk classTypeRender = className(classLikeType.getName());
return classLikeType.isDefinition() ? wrapInTypingType(classTypeRender) : classTypeRender;
}
@Override
public HtmlChunk visitPyNarrowedType(@NotNull PyNarrowedType narrowedType) {
HtmlBuilder result = new HtmlBuilder();
if (narrowedType.getTypeIs()) {
result.append(styled("TypeIs", PyHighlighter.PY_CLASS_DEFINITION));
}
else {
result.append(styled("TypeGuard", PyHighlighter.PY_CLASS_DEFINITION));
}
result.append(styled("[", PyHighlighter.PY_BRACKETS));
result.append(render(narrowedType.getNarrowedType()));
result.append(styled("]", PyHighlighter.PY_BRACKETS));
return result.toFragment();
}
@Override
public HtmlChunk visitPyUnionType(@NotNull PyUnionType unionType) {
// TODO Exclude "Unknown" once it's introduced, don't exclude explicit typing.Any
if (isOptional(unionType)) {
return renderOptional(unionType);
}
Pair<List<PyLiteralType>, List<PyType>> literalsAndOthers = extractLiterals(unionType);
if (literalsAndOthers != null) {
if (literalsAndOthers.second.isEmpty()) {
return renderUnionOfLiterals(literalsAndOthers.first);
}
return renderUnion(ContainerUtil.prepend(
ContainerUtil.map(literalsAndOthers.second, this::render),
renderUnionOfLiterals(literalsAndOthers.first)
));
}
if (ContainerUtil.all(unionType.getMembers(), t -> t instanceof PyClassType ct && ct.isDefinition())) {
return wrapInTypingType(render(unionType.map(type -> type != null ? ((PyClassType)type).toInstance() : null)));
}
// TODO Remove special-casing of "weak" unions
// Exclude Any from "weak" types
if (PyTypeChecker.isUnknown(unionType, false, myTypeEvalContext)) {
if (hideWeakUnions()) {
return render(unionType.excludeNull());
}
// Always put Any at the end of the union
return renderUnion(List.of(render(unionType.excludeNull()), visitUnknownType()));
}
return renderUnion(ContainerUtil.map(unionType.getMembers(), this::render));
}
private @NotNull HtmlChunk renderUnionOfLiterals(@NotNull List<PyLiteralType> literals) {
return new HtmlBuilder()
.append(escaped("Literal")) //NON-NLS
.append(styled("[", PyHighlighter.PY_BRACKETS))
.append(StreamEx
.of(literals)
.map(PyLiteralType::getExpression)
.map(expr -> styledExpression(expr))
.collect(HtmlChunk.toFragment(styled(", ", PyHighlighter.PY_COMMA))))
.append(styled("]", PyHighlighter.PY_BRACKETS))
.toFragment();
}
private @NotNull HtmlChunk renderUnion(@NotNull List<HtmlChunk> renderedUnionMembers) {
HtmlBuilder result = new HtmlBuilder();
if (isBitwiseOrUnionAvailable()) {
result.append(renderList(renderedUnionMembers, " | "));
}
else {
result.append(escaped("Union")); //NON-NLS
result.append(styled("[", PyHighlighter.PY_BRACKETS));
result.append(renderList(renderedUnionMembers));
result.append(styled("]", PyHighlighter.PY_BRACKETS));
}
return result.toFragment();
}
// TODO get rid of dedicated rendering for Optional
private @NotNull HtmlChunk renderOptional(@NotNull PyUnionType type) {
HtmlBuilder result = new HtmlBuilder();
if (isBitwiseOrUnionAvailable()) {
result.append(render(ContainerUtil.find(type.getMembers(), t -> !isNoneType(t))));
result.append(styled(" | ", PyHighlighter.PY_OPERATION_SIGN));
// TODO make this rendering consistent with rendering on None as a PyClassType instance
result.append(toClass(PyNames.TYPE_NONE, "None", PyHighlighter.PY_KEYWORD)); //NON-NLS
}
else {
result.append(escaped("Optional")); //NON-NLS
result.append(styled("[", PyHighlighter.PY_BRACKETS));
result.append(render(ContainerUtil.find(type.getMembers(), t -> !isNoneType(t))));
result.append(styled("]", PyHighlighter.PY_BRACKETS));
}
return result.toFragment();
}
private static @Nullable Pair<@NotNull List<PyLiteralType>, @NotNull List<PyType>> extractLiterals(@NotNull PyUnionType type) {
final Collection<PyType> members = type.getMembers();
final List<PyLiteralType> literalTypes = ContainerUtil.filterIsInstance(members, PyLiteralType.class);
if (literalTypes.size() < 2) return null;
final List<PyType> otherTypes = ContainerUtil.filter(members, m -> !(m instanceof PyLiteralType));
return Pair.create(literalTypes, otherTypes);
}
private static boolean isOptional(@NotNull PyUnionType type) {
return type.getMembers().size() == 2 && ContainerUtil.find(type.getMembers(), m -> isNoneType(m)) != null;
}
@Override
public HtmlChunk visitPyTupleType(@NotNull PyTupleType tupleType) {
HtmlBuilder result = new HtmlBuilder();
if (isGenericBuiltinsAvailable()) {
result.append(styled("tuple", PyHighlighter.PY_BUILTIN_NAME)); //NON-NLS
}
else {
result.append(escaped("Tuple")); //NON-NLS
}
result.append(styled("[", PyHighlighter.PY_BRACKETS));
if (!tupleType.getElementTypes().isEmpty()) {
result.append(renderList(ContainerUtil.map(tupleType.getElementTypes(), this::render)));
if (tupleType.isHomogeneous()) {
result.append(styled(", ", PyHighlighter.PY_COMMA));
result.append(styled("...", PyHighlighter.PY_DOT));
}
}
else {
result.append(styled("()", PyHighlighter.PY_PARENTHS));
}
result.append(styled("]", PyHighlighter.PY_BRACKETS));
return result.toFragment();
}
@Override
public HtmlChunk visitUnknownType() {
return HtmlChunk.raw("Any"); //NON-NLS
}
@Override
public HtmlChunk visitPyType(@NotNull PyType type) {
return escaped(type.getName());
}
@Override
public HtmlChunk visitPyCallableType(@NotNull PyCallableType callableType) {
HtmlBuilder result = new HtmlBuilder();
result.append(styled("(", PyHighlighter.PY_PARENTHS));
List<PyCallableParameter> parameters = callableType.getParameters(myTypeEvalContext);
if (parameters != null) {
result.append(renderList(ContainerUtil.map(parameters, this::visitPyCallableParameter)));
}
else {
result.append(styled("...", PyHighlighter.PY_DOT));
}
result.append(styled(")", PyHighlighter.PY_PARENTHS));
result.append(escaped(" -> "));
result.append(render(callableType.getReturnType(myTypeEvalContext)));
return result.toFragment();
}
@NotNull
protected HtmlChunk visitPyCallableParameter(@NotNull PyCallableParameter param) {
HtmlBuilder result = new HtmlBuilder();
PyType type = param.getType(myTypeEvalContext);
// TODO remove that
if (!(type instanceof PyParamSpecType) && !(type instanceof PyConcatenateType)) {
if (param.getName() != null) {
result.append(styled(param.getName(), PyHighlighter.PY_PARAMETER));
result.append(styled(": ", PyHighlighter.PY_OPERATION_SIGN));
}
}
result.append(render(type));
return result.toFragment();
}
protected @NotNull HtmlChunk renderList(@NotNull Collection<HtmlChunk> list) {
return renderList(list, ", ");
}
protected @NotNull HtmlChunk renderList(@NotNull Collection<HtmlChunk> list, @NotNull @Nls String separator) {
return StreamEx.of(list).collect(HtmlChunk.toFragment(switch (separator) {
case ", " -> {
yield styled(separator, PyHighlighter.PY_COMMA);
}
case " | " -> {
yield styled(separator, PyHighlighter.PY_OPERATION_SIGN);
}
default -> {
yield escaped(separator);
}
}));
}
@Override
public @NotNull HtmlChunk visitPyTypeVarType(@NotNull PyTypeVarType typeVarType) {
HtmlChunk typeVarTypeRender = escaped(typeVarType.getName());
return typeVarType.isDefinition() ? wrapInTypingType(typeVarTypeRender) : typeVarTypeRender;
}
@Override
public HtmlChunk visitPyTypeParameterType(@NotNull PyTypeParameterType typeParameterType) {
return escaped(typeParameterType.getName());
}
@Override
public @NotNull HtmlChunk visitPySelfType(@NotNull PySelfType selfType) {
// Don't render Self as a type parameter
return className(selfType.getName());
}
@Override
public @NotNull HtmlChunk visitPyCallableParameterListType(@NotNull PyCallableParameterListType callableParameterListType) {
HtmlBuilder result = new HtmlBuilder();
result.append("[");
result.append(renderList(ContainerUtil.map(callableParameterListType.getParameters(), this::visitPyCallableParameter)));
result.append("]");
return result.toFragment();
}
}

View File

@@ -334,14 +334,14 @@ public class PythonDocumentationProvider implements DocumentationProvider {
* @return string representation of the type
*/
public static @NotNull @NlsSafe String getTypeName(@Nullable PyType type, @NotNull TypeEvalContext context) {
return buildTypeModel(type, context).asString();
return PyTypeVisitor.visit(type, new PyTypeRenderer.Documentation(context, false)).toString();
}
/**
* Returns the provided type in PEP 484 compliant format.
*/
public static @NotNull String getTypeHint(@Nullable PyType type, @NotNull TypeEvalContext context) {
return buildTypeModel(type, context).asPep484TypeHint();
return PyTypeVisitor.visit(type, new PyTypeRenderer.TypeHint(context)).toString();
}
/**
@@ -353,7 +353,7 @@ public class PythonDocumentationProvider implements DocumentationProvider {
* such as bounds for TypeVar types in ' ≤: *bound*' format
*/
public static @NotNull String getVerboseTypeName(@Nullable PyType type, @NotNull TypeEvalContext context) {
return buildTypeModel(type, context).asStringWithAdditionalInfo();
return PyTypeVisitor.visit(type, new PyTypeRenderer.VerboseDocumentation(context)).toString();
}
/**
@@ -378,7 +378,7 @@ public class PythonDocumentationProvider implements DocumentationProvider {
return;
}
}
buildTypeModel(type, context).toBodyWithLinks(body, anchor);
body.append(PyTypeVisitor.visit(type, new PyTypeRenderer.RichDocumentation(context, anchor)));
}
/**
@@ -388,11 +388,7 @@ public class PythonDocumentationProvider implements DocumentationProvider {
* {@code Any} is excluded from {@code Union[Any, ...]}-like types.
*/
public static @NotNull String getTypeDescription(@Nullable PyType type, @NotNull TypeEvalContext context) {
return buildTypeModel(type, context).asDescription();
}
private static @NotNull PyTypeModelBuilder.TypeModel buildTypeModel(@Nullable PyType type, @NotNull TypeEvalContext context) {
return new PyTypeModelBuilder(context).build(type, true);
return PyTypeVisitor.visit(type, new PyTypeRenderer.Documentation(context, true)).toString();
}
static @NotNull HtmlChunk describeDecorators(@NotNull PyDecoratable decoratable,

View File

@@ -37,9 +37,9 @@ expects_type(object)
expects_typing_type(type)
expects_typing_type_any(type)
expects_typing_type(object)
expects_str_class(<warning descr="Expected type 'type[str]', got 'type' instead">type</warning>)
expects_str_class(<warning descr="Expected type 'type[str]', got 'type[type]' instead">type</warning>)
expects_any_type_via_type_var(type)
expects_str_subclass(<warning descr="Expected type 'type[T ≤: str]', got 'type' instead">type</warning>)
expects_str_subclass(<warning descr="Expected type 'type[T ≤: str]', got 'type[type]' instead">type</warning>)
expects_object(type)

View File

@@ -0,0 +1,4 @@
from typing import Literal
HTML_ESCAPES: set[Literal["<", ">", "&"]] = {"<", ">", "&"}
v<caret>ar = HTML_ESCAPES

View File

@@ -0,0 +1,4 @@
from typing import Literal, Set, Union
HTML_ESCAPES: set[Literal["<", ">", "&"]] = {"<", ">", "&"}
var: [Set[Literal["<", ">", "&"]]] = HTML_ESCAPES

View File

@@ -1051,7 +1051,7 @@ public class PyTypeTest extends PyTestCase {
}
public void testDictFromTuple() {
doTest("Dict[Union[str, int], Union[str, int]]",
doTest("Dict[Union[str, int], Union[int, str]]",
"expr = dict((('1', 1), (2, 2), (3, '3')))");
}
@@ -1662,9 +1662,9 @@ public class PyTypeTest extends PyTestCase {
doTest("Dict[int, bool]", "expr = {1: False}");
doTest("Dict[Union[str, int], Union[str, int]]", "expr = {'1': 1, 1: '1', 1: 1}");
doTest("Dict[Union[str, int], Union[int, str]]", "expr = {'1': 1, 1: '1', 1: 1}");
doTest("Dict[Union[Union[str, int], Any], Union[Union[str, int], Any]]",
doTest("Dict[Union[Union[str, int], Any], Union[Union[int, str], Any]]",
"expr = {'1': 1, 1: '1', 1: 1, 1: 1, 1: 1, 1: 1, 1: 1, 1: 1, 1: 1, 1: 1, 1: 1}");
}
@@ -3980,7 +3980,7 @@ public class PyTypeTest extends PyTestCase {
runWithLanguageLevel(
LanguageLevel.getLatest(),
() -> {
doTest("A",
doTest("type[A]",
"""
from typing import TypedDict
A = TypedDict('A', {'x': int}, total=False)

View File

@@ -306,6 +306,10 @@ public class PyAnnotateVariableTypeIntentionTest extends PyIntentionTestCase {
doTest(LanguageLevel.PYTHON36);
}
public void testAnnotationLiteralTypeWithIllegalHtmlCharacters() {
doAnnotationTest();
}
private void doAnnotationTest() {
doTest(LanguageLevel.PYTHON36);
}