Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
Type,
TypedDictType,
TypeOfAny,
TypeOfLiteralString,
TypeType,
TypeVarType,
UninhabitedType,
Expand Down Expand Up @@ -2724,7 +2725,8 @@ def infer_literal_expr_type(self, value: LiteralValue, fallback_name: str) -> Ty
return typ.copy_modified(
last_known_value=LiteralType(
value=value, fallback=typ, line=typ.line, column=typ.column
)
),
literal_string=TypeOfLiteralString.implicit,
)

def concat_tuples(self, left: TupleType, right: TupleType) -> TupleType:
Expand Down
6 changes: 6 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
TypeAliasType,
TypedDictType,
TypeOfAny,
TypeOfLiteralString,
TypeType,
TypeVarType,
UnboundType,
Expand Down Expand Up @@ -2212,6 +2213,11 @@ def format_literal_value(typ: LiteralType) -> str:
if itype.extra_attrs and itype.extra_attrs.mod_name and module_names:
return f"{base_str} {itype.extra_attrs.mod_name}"
return base_str
elif (
itype.type.fullname == "builtins.str"
and itype.literal_string == TypeOfLiteralString.explicit
):
return "LiteralString"
if verbosity >= 2 or (fullnames and itype.type.fullname in fullnames):
base_str = itype.type.fullname
else:
Expand Down
9 changes: 1 addition & 8 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ def get_column(self) -> int:
"typing.DefaultDict": "collections.defaultdict",
"typing.Deque": "collections.deque",
"typing.OrderedDict": "collections.OrderedDict",
# HACK: a lie in lieu of actual support for PEP 675
"typing.LiteralString": "builtins.str",
}

# This keeps track of the oldest supported Python version where the corresponding
Expand All @@ -154,15 +152,12 @@ def get_column(self) -> int:
"typing.DefaultDict": (2, 7),
"typing.Deque": (2, 7),
"typing.OrderedDict": (3, 7),
"typing.LiteralString": (3, 11),
}

# This keeps track of aliases in `typing_extensions`, which we treat specially.
typing_extensions_aliases: Final = {
# See: https://github.com/python/mypy/issues/11528
"typing_extensions.OrderedDict": "collections.OrderedDict",
# HACK: a lie in lieu of actual support for PEP 675
"typing_extensions.LiteralString": "builtins.str",
"typing_extensions.OrderedDict": "collections.OrderedDict"
}

reverse_builtin_aliases: Final = {
Expand All @@ -176,8 +171,6 @@ def get_column(self) -> int:
_nongen_builtins.update((name, alias) for alias, name in type_aliases.items())
# Drop OrderedDict from this for backward compatibility
del _nongen_builtins["collections.OrderedDict"]
# HACK: consequence of hackily treating LiteralString as an alias for str
del _nongen_builtins["builtins.str"]


def get_nongen_builtins(python_version: tuple[int, int]) -> dict[str, str]:
Expand Down
3 changes: 2 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
ASSERT_TYPE_NAMES,
FINAL_DECORATOR_NAMES,
FINAL_TYPE_NAMES,
LITERAL_STRING_NAMES,
NEVER_NAMES,
OVERLOAD_NAMES,
PROTOCOL_NAMES,
Expand Down Expand Up @@ -2655,7 +2656,7 @@ def is_type_ref(self, rv: Expression, bare: bool = False) -> bool:
if bare:
# These three are valid even if bare, for example
# A = Tuple is just equivalent to A = Tuple[Any, ...].
valid_refs = {"typing.Any", "typing.Tuple", "typing.Callable"}
valid_refs = {"typing.Any", "typing.Tuple", "typing.Callable", *LITERAL_STRING_NAMES}
else:
valid_refs = type_constructors

Expand Down
21 changes: 21 additions & 0 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
TypeAliasType,
TypedDictType,
TypeOfAny,
TypeOfLiteralString,
TypeType,
TypeVarTupleType,
TypeVarType,
Expand Down Expand Up @@ -463,7 +464,20 @@ def visit_instance(self, left: Instance) -> bool:
# and we can't have circular promotions.
if left.type.alt_promote is right.type:
return True

rname = right.type.fullname

# Check `LiteralString` special case:
if (
rname == "builtins.str"
and right.literal_string == TypeOfLiteralString.explicit
and left.type.fullname == rname
):
return left.literal_string is not None or (
left.last_known_value is not None
and isinstance(left.last_known_value.value, str)
)

# Always try a nominal check if possible,
# there might be errors that a user wants to silence *once*.
# NamedTuples are a special case, because `NamedTuple` is not listed
Expand Down Expand Up @@ -773,6 +787,13 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool:
def visit_literal_type(self, left: LiteralType) -> bool:
if isinstance(self.right, LiteralType):
return left == self.right
elif (
isinstance(left.value, str)
and isinstance(self.right, Instance)
and self.right.type.fullname == "builtins.str"
and self.right.literal_string is not None
):
return True
else:
return self._is_subtype(left.fallback, self.right)

Expand Down
8 changes: 2 additions & 6 deletions mypy/type_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,8 @@ def visit_instance(self, t: Instance) -> Type:
raw_last_known_value = t.last_known_value.accept(self)
assert isinstance(raw_last_known_value, LiteralType) # type: ignore[misc]
last_known_value = raw_last_known_value
return Instance(
typ=t.type,
args=self.translate_types(t.args),
line=t.line,
column=t.column,
last_known_value=last_known_value,
return t.copy_modified(
args=self.translate_types(t.args), last_known_value=last_known_value
)

def visit_type_var(self, t: TypeVarType) -> Type:
Expand Down
11 changes: 10 additions & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from mypy.types import (
ANNOTATED_TYPE_NAMES,
FINAL_TYPE_NAMES,
LITERAL_STRING_NAMES,
LITERAL_TYPE_NAMES,
NEVER_NAMES,
TYPE_ALIAS_NAMES,
Expand Down Expand Up @@ -74,6 +75,7 @@
TypedDictType,
TypeList,
TypeOfAny,
TypeOfLiteralString,
TypeQuery,
TypeType,
TypeVarLikeType,
Expand Down Expand Up @@ -574,6 +576,10 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
self.fail('"Unpack" is not supported yet, use --enable-incomplete-features', t)
return AnyType(TypeOfAny.from_error)
return UnpackType(self.anal_type(t.args[0]), line=t.line, column=t.column)
elif fullname in LITERAL_STRING_NAMES:
inst = self.named_type("builtins.str")
inst.literal_string = TypeOfLiteralString.explicit
return inst
return None

def get_omitted_any(self, typ: Type, fullname: str | None = None) -> AnyType:
Expand Down Expand Up @@ -1587,7 +1593,10 @@ def expand_type_alias(
assert isinstance(node.target, Instance) # type: ignore[misc]
# Note: this is the only case where we use an eager expansion. See more info about
# no_args aliases like L = List in the docstring for TypeAlias class.
return Instance(node.target.type, [], line=ctx.line, column=ctx.column)
inst = node.target.copy_modified(args=[])
inst.line = ctx.line
inst.column = ctx.column
return inst
return TypeAliasType(node, [], line=ctx.line, column=ctx.column)
if (
exp_len == 0
Expand Down
44 changes: 40 additions & 4 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@

OVERLOAD_NAMES: Final = ("typing.overload", "typing_extensions.overload")

LITERAL_STRING_NAMES: Final = ("typing.LiteralString", "typing_extensions.LiteralString")

# Attributes that can optionally be defined in the body of a subclass of
# enum.Enum but are removed from the class __dict__ by EnumMeta.
ENUM_REMOVED_PROPS: Final = ("_ignore_", "_order_", "__order__")
Expand Down Expand Up @@ -1193,6 +1195,15 @@ def copy(self) -> ExtraAttrs:
return ExtraAttrs(self.attrs.copy(), self.immutable.copy(), self.mod_name)


class TypeOfLiteralString:
"""Used to specify what kind of `LiteralString` are we dealing with."""

__slots__ = ()

explicit: Final = 1
implicit: Final = 2


class Instance(ProperType):
"""An instance type of form C[T1, ..., Tn].

Expand All @@ -1203,7 +1214,16 @@ class Instance(ProperType):
fallbacks for all "non-special" (like UninhabitedType, ErasedType etc) types.
"""

__slots__ = ("type", "args", "invalid", "type_ref", "last_known_value", "_hash", "extra_attrs")
__slots__ = (
"type",
"args",
"invalid",
"type_ref",
"last_known_value",
"_hash",
"extra_attrs",
"literal_string",
)

def __init__(
self,
Expand All @@ -1214,6 +1234,7 @@ def __init__(
*,
last_known_value: LiteralType | None = None,
extra_attrs: ExtraAttrs | None = None,
literal_string: int | None = None,
) -> None:
super().__init__(line, column)
self.type = typ
Expand Down Expand Up @@ -1276,6 +1297,11 @@ def __init__(
# to be "short-lived", we don't serialize it, and even don't store as variable type.
self.extra_attrs = extra_attrs

# Is set to `1` when explicit `LiteralString` type is used.
# Is set to `2` when implicit `LiteralString` is used, like `'a'`
# Is `None` by default.
self.literal_string = literal_string

def accept(self, visitor: TypeVisitor[T]) -> T:
return visitor.visit_instance(self)

Expand All @@ -1292,6 +1318,7 @@ def __eq__(self, other: object) -> bool:
and self.args == other.args
and self.last_known_value == other.last_known_value
and self.extra_attrs == other.extra_attrs
and self.literal_string == self.literal_string
)

def serialize(self) -> JsonDict | str:
Expand All @@ -1304,6 +1331,7 @@ def serialize(self) -> JsonDict | str:
data["args"] = [arg.serialize() for arg in self.args]
if self.last_known_value is not None:
data["last_known_value"] = self.last_known_value.serialize()
data["literal_string"] = self.literal_string
return data

@classmethod
Expand All @@ -1322,22 +1350,27 @@ def deserialize(cls, data: JsonDict | str) -> Instance:
inst.type_ref = data["type_ref"] # Will be fixed up by fixup.py later.
if "last_known_value" in data:
inst.last_known_value = LiteralType.deserialize(data["last_known_value"])
inst.literal_string = data["literal_string"]
return inst

def copy_modified(
self,
*,
args: Bogus[list[Type]] = _dummy,
last_known_value: Bogus[LiteralType | None] = _dummy,
literal_string: Bogus[int | None] = _dummy,
) -> Instance:
new = Instance(
self.type,
args if args is not _dummy else self.args,
self.line,
self.column,
last_known_value=last_known_value
if last_known_value is not _dummy
else self.last_known_value,
last_known_value=(
last_known_value if last_known_value is not _dummy else self.last_known_value
),
literal_string=(
literal_string if literal_string is not _dummy else self.literal_string
),
)
# We intentionally don't copy the extra_attrs here, so they will be erased.
new.can_be_true = self.can_be_true
Expand Down Expand Up @@ -2901,6 +2934,9 @@ def visit_instance(self, t: Instance) -> str:
else:
s = t.type.fullname or t.type.name or "<???>"

if t.literal_string == TypeOfLiteralString.explicit:
s = "LiteralString"

if t.args:
if t.type.fullname == "builtins.tuple":
assert len(t.args) == 1
Expand Down
2 changes: 2 additions & 0 deletions mypy/typestate.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ def is_cached_subtype_check(kind: SubtypeKind, left: Instance, right: Instance)
# will be an unbounded number of potential types to cache,
# making caching less effective.
return False
if left.literal_string or right.literal_string:
return False
info = right.type
cache = TypeState._subtype_caches.get(info)
if cache is None:
Expand Down
Loading