Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add initial LiteralString support #13664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 @@ -144,6 +144,7 @@
TypeAliasType,
TypedDictType,
TypeOfAny,
TypeOfLiteralString,
TypeType,
TypeVarType,
UninhabitedType,
Expand Down Expand Up @@ -2727,7 +2728,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 @@ -2240,6 +2241,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 @@ -234,6 +234,7 @@
ASSERT_TYPE_NAMES,
FINAL_DECORATOR_NAMES,
FINAL_TYPE_NAMES,
LITERAL_STRING_NAMES,
NEVER_NAMES,
OVERLOAD_NAMES,
PROTOCOL_NAMES,
Expand Down Expand Up @@ -2679,7 +2680,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 @@ -575,6 +577,10 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
self.fail("Unpack[...] requires exactly one type argument", 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 @@ -1602,7 +1608,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 @@ -1196,6 +1198,15 @@ def __repr__(self) -> str:
return f"ExtraAttrs({self.attrs!r}, {self.immutable!r}, {self.mod_name!r})"


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 @@ -1206,7 +1217,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 @@ -1217,6 +1237,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 @@ -1279,6 +1300,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 @@ -1295,6 +1321,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 @@ -1307,6 +1334,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 @@ -1325,22 +1353,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 @@ -2904,6 +2937,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