Skip to content

Commit

Permalink
Change the error message
Browse files Browse the repository at this point in the history
And fix NamedTuple once more
  • Loading branch information
tmke8 committed Nov 4, 2022
1 parent 5059c64 commit 208e0a0
Show file tree
Hide file tree
Showing 11 changed files with 87 additions and 58 deletions.
52 changes: 35 additions & 17 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1359,43 +1359,61 @@ def check_callable_call(
# An Enum() call that failed SemanticAnalyzerPass2.check_enum_call().
return callee.ret_type, callee

type = callee.type_object() if callee.is_type_obj() else None
typ = callee.type_object() if callee.is_type_obj() else None
if (
type is not None
and type.is_protocol
typ is not None
and typ.is_protocol
# Exception for Type[...]
and not callee.from_type_type
):
self.chk.fail(message_registry.CANNOT_INSTANTIATE_PROTOCOL.format(type.name), context)
self.chk.fail(message_registry.CANNOT_INSTANTIATE_PROTOCOL.format(typ.name), context)
elif (
type is not None
and type.is_abstract
typ is not None
and typ.is_abstract
# Exception for Type[...]
and not callee.from_type_type
and not callee.type_object().fallback_to_any
):
# Determine whether the implicitly abstract attributes are functions with
# None-compatible return types.
abstract_attributes: dict[str, bool] = {}
for attr_name, abstract_status in type.abstract_attributes:
for attr_name, abstract_status in typ.abstract_attributes:
if abstract_status == IMPLICITLY_ABSTRACT:
abstract_attributes[attr_name] = self.can_return_none(type, attr_name)
abstract_attributes[attr_name] = self.can_return_none(typ, attr_name)
else:
abstract_attributes[attr_name] = False
self.msg.cannot_instantiate_abstract_class(type.name, abstract_attributes, context)
self.msg.cannot_instantiate_abstract_class(typ.name, abstract_attributes, context)
elif (
type is not None
and type.uninitialized_vars
typ is not None
and typ.uninitialized_vars
# Exception for Type[...]
and not callee.from_type_type
and not callee.type_object().fallback_to_any
):
self.chk.fail(
message_registry.CLASS_HAS_UNINITIALIZED_VARS.format(
type.name, format_string_list([f'"{v}"' for v in type.uninitialized_vars])
),
context,
)
# Split the list of variables into instance and class vars.
ivars: list[str] = []
cvars: list[str] = []
for uninitialized in typ.uninitialized_vars:
for base in typ.mro:
symnode = base.names.get(uninitialized)
if symnode is None:
continue
node = symnode.node
if isinstance(node, Var):
if node.is_classvar:
cvars.append(uninitialized)
else:
ivars.append(uninitialized)
break
for vars, kind in [(ivars, "instance"), (cvars, "class")]:
if not vars:
continue
self.chk.fail(
message_registry.CLASS_HAS_UNINITIALIZED_VARS.format(
typ.name, kind, format_string_list([f'"{v}"' for v in vars], "attributes")
),
context,
)

formal_to_actual = map_actuals_to_formals(
arg_kinds,
Expand Down
2 changes: 1 addition & 1 deletion mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
)
NOT_CALLABLE: Final = "{} not callable"
TYPE_MUST_BE_USED: Final = "Value of type {} must be used"
CLASS_HAS_UNINITIALIZED_VARS: Final = 'Class "{}" has annotated but unset attributes: {}'
CLASS_HAS_UNINITIALIZED_VARS: Final = 'Class "{}" has annotated but unset {} attributes: {}'

# Generic
GENERIC_INSTANCE_VAR_CLASS_ACCESS: Final = (
Expand Down
5 changes: 3 additions & 2 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2694,17 +2694,18 @@ def strip_quotes(s: str) -> str:
return s


def format_string_list(lst: list[str]) -> str:
def format_string_list(lst: list[str], list_objects: str = "methods") -> str:
assert len(lst) > 0
if len(lst) == 1:
return lst[0]
elif len(lst) <= 5:
return f"{', '.join(lst[:-1])} and {lst[-1]}"
else:
return "%s, ... and %s (%i methods suppressed)" % (
return "%s, ... and %s (%i %s suppressed)" % (
", ".join(lst[:2]),
lst[-1],
len(lst) - 3,
list_objects,
)


Expand Down
6 changes: 3 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ def deserialize(cls, data: JsonDict) -> Decorator:
"is_settable_property",
"is_suppressed_import",
"is_classvar",
"is_abstract_var",
"is_uninitialized",
"is_final",
"final_unset_in_class",
"final_set_in_init",
Expand Down Expand Up @@ -993,7 +993,7 @@ class Var(SymbolNode):
"is_property",
"is_settable_property",
"is_classvar",
"is_abstract_var",
"is_uninitialized",
"is_final",
"final_unset_in_class",
"final_set_in_init",
Expand Down Expand Up @@ -1026,7 +1026,7 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None:
self.is_property = False
self.is_settable_property = False
self.is_classvar = False
self.is_abstract_var = False
self.is_uninitialized = False
# Set to true when this variable refers to a module we were unable to
# parse for some reason (eg a silenced module)
self.is_suppressed_import = False
Expand Down
2 changes: 1 addition & 1 deletion mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ def _analyze_class(
continue
assert isinstance(node, Var)
node.is_initialized_in_class = False
node.is_abstract_var = False
node.is_uninitialized = False

# Traverse the MRO and collect attributes from the parents.
taken_attr_names = set(own_attrs)
Expand Down
2 changes: 1 addition & 1 deletion mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
continue

# In dataclasses, attributes are initialized in the generated __init__.
node.is_abstract_var = False
node.is_uninitialized = False

# x: InitVar[int] is turned into x: int and is removed from the class.
is_init_var = False
Expand Down
14 changes: 8 additions & 6 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3119,8 +3119,10 @@ def process_type_annotation(self, s: AssignmentStmt) -> None:
and isinstance(s.rvalue, TempNode)
and s.rvalue.no_rhs
):
# If this is a protocol or if `warn_uninitialized_attributes` is set,
# we keep track of uninitialized variables.
if isinstance(lvalue.node, Var):
lvalue.node.is_abstract_var = True
lvalue.node.is_uninitialized = True
else:
if (
self.type
Expand Down Expand Up @@ -3662,7 +3664,7 @@ def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool, is_final:
node is None
or (
isinstance(node.node, Var)
and node.node.is_abstract_var
and node.node.is_uninitialized
and not defined_in_this_class
)
# ... also an explicit declaration on self also creates a new Var.
Expand All @@ -3688,11 +3690,11 @@ def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool, is_final:
self.type.names[lval.name] = SymbolTableNode(MDEF, v, implicit=True)
elif (
isinstance(node.node, Var)
and node.node.is_abstract_var
and node.node.is_uninitialized
and not self.type.is_protocol
):
# We don't need to create a new Var, but we need to mark it as non-abstract.
node.node.is_abstract_var = False
# Something is assigned to this variable here, so we mark it as initialized.
node.node.is_uninitialized = False
self.check_lvalue_validity(lval.node, lval)

def is_self_member_ref(self, memberexpr: MemberExpr) -> bool:
Expand Down Expand Up @@ -4067,7 +4069,7 @@ def check_classvar(self, s: AssignmentStmt) -> None:
if len(s.lvalues) != 1 or not isinstance(lvalue, RefExpr):
return
if self.is_class_scope() and isinstance(lvalue, NameExpr) and isinstance(lvalue.node, Var):
lvalue.node.is_abstract_var = False
lvalue.node.is_uninitialized = False
if not s.type or not self.is_classvar(s.type):
return
if self.is_class_scope() and isinstance(lvalue, NameExpr):
Expand Down
8 changes: 4 additions & 4 deletions mypy/semanal_classprop.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ def calculate_class_abstract_status(
if base is typ:
abstract_in_this_class.append(name)
elif isinstance(node, Var):
if node.is_abstract_var and name not in concrete:
# If this abstract variable comes from a protocol, then `typ`
# is abstract.
if node.is_uninitialized and name not in concrete:
# If this uninitialized variable comes from a protocol, then we
# treat it as an "abstract" variable and mark `typ` as abstract.
if base.is_protocol:
typ.is_abstract = True
abstract.append((name, IS_ABSTRACT))
Expand All @@ -99,7 +99,7 @@ def calculate_class_abstract_status(
uninitialized_vars.add(name)
elif (
options.warn_uninitialized_attributes
and not node.is_abstract_var
and not node.is_uninitialized
and name in uninitialized_vars
):
# A variable can also be initialized in a parent class.
Expand Down
4 changes: 2 additions & 2 deletions mypy/semanal_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ def restore_saved_attrs(saved_attrs: SavedAttributes) -> None:
if (
existing is None
or
# (An abstract Var is considered as not defined.)
# (An uninitialized Var is considered as not defined.)
(
isinstance(existing.node, Var)
and existing.node.is_abstract_var
and existing.node.is_uninitialized
and not defined_in_this_class
)
or
Expand Down
8 changes: 4 additions & 4 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,15 +647,15 @@ def save_namedtuple_body(self, named_tuple_info: TypeInfo) -> Iterator[None]:
# Keep user-defined methods as is.
continue
elif isinstance(sym.node, Var):
# NamedTuple fields are always initialized, so they are not abstract.
sym.node.is_abstract_var = False
# NamedTuple fields are initialized in the generated __init__.
sym.node.is_uninitialized = False
# Keep existing (user-provided) definitions under mangled names, so they
# get semantically analyzed.
r_key = get_unique_redefinition_name(key, named_tuple_info.names)
named_tuple_info.names[r_key] = sym
if isinstance(value.node, Var):
# NamedTuple fields are always initialized, so they are not abstract.
value.node.is_abstract_var = False
# NamedTuple fields are initialized in the generated __init__.
value.node.is_uninitialized = False
named_tuple_info.names[key] = value

# Helpers
Expand Down
42 changes: 25 additions & 17 deletions test-data/unit/check-warnings.test
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,13 @@ class A:
# flags: --warn-uninitialized-attributes
class A:
x: int
a = A() # E: Class "A" has annotated but unset attributes: "x"
a = A() # E: Class "A" has annotated but unset instance attributes: "x"
class B(A):
def __init__(self) -> None:
self.x = 10
B() # OK
class C(A): ...
C() # E: Class "C" has annotated but unset attributes: "x"
C() # E: Class "C" has annotated but unset instance attributes: "x"
class D(A):
def f(self) -> None:
self.x = "foo" # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int")
Expand Down Expand Up @@ -273,32 +273,36 @@ from abc import ABC

class A(ABC):
x: str
A() # E: Class "A" has annotated but unset attributes: "x"
A() # E: Class "A" has annotated but unset instance attributes: "x"
class B(A): ...
B() # E: Class "B" has annotated but unset attributes: "x"
B() # E: Class "B" has annotated but unset instance attributes: "x"
class C(A):
def f(self) -> None:
self.x = "foo"
C()

[case testUninitializedAttributeMultiple]
# flags: --warn-uninitialized-attributes
from typing import ClassVar
class A:
x: int
y: str = "foo"
z: bool
A() # E: Class "A" has annotated but unset attributes: "x" and "z"
c: ClassVar[int]
A() # E: Class "A" has annotated but unset instance attributes: "x" and "z" \
# E: Class "A" has annotated but unset class attributes: "c"
class B(A):
c = 0
def __init__(self) -> None:
self.x = 3
B() # E: Class "B" has annotated but unset attributes: "z"
B() # E: Class "B" has annotated but unset instance attributes: "z"
class C(B):
def __init__(self) -> None:
self.z = True
C() # This should actually throw an error because `super().__init__` wasn't called.
C() # This should ideally be an error because `super().__init__` wasn't called.

[case testUninitializedAttributeDataclass]
# flags: --warn-uninitialized-attributes --python-version 3.7
# flags: --warn-uninitialized-attributes
from dataclasses import InitVar, dataclass
from typing import ClassVar

Expand All @@ -310,7 +314,7 @@ class A:
@classmethod
def f(cls) -> None:
cls.z = True
A(3, "foo") # E: Class "A" has annotated but unset attributes: "z"
A(3, "foo") # E: Class "A" has annotated but unset class attributes: "z"
reveal_type(A.z) # N: Revealed type is "builtins.bool"
@dataclass
class A2(A):
Expand All @@ -323,7 +327,7 @@ class B:

@dataclass
class C(B): ...
C() # E: Class "C" has annotated but unset attributes: "x" and "y"
C() # E: Class "C" has annotated but unset instance attributes: "x" and "y"

@dataclass
class D(B):
Expand All @@ -341,15 +345,15 @@ from typing import ClassVar
class A:
x: int
z: ClassVar[bool]
A(3) # E: Class "A" has annotated but unset attributes: "z"
A(3) # E: Class "A" has annotated but unset class attributes: "z"

class B:
x: int
y: str

@attr.define
class C(B): ...
C() # E: Class "C" has annotated but unset attributes: "x" and "y"
C() # E: Class "C" has annotated but unset instance attributes: "x" and "y"

@attr.define
class D(B):
Expand All @@ -362,9 +366,13 @@ D(3, "foo")
# flags: --warn-uninitialized-attributes
from typing import NamedTuple

class A(NamedTuple):
x: int
A(3)
class Employee(NamedTuple):
name: str
id: int = 3

def __repr__(self) -> str:
return self.name
Employee("foo")
[builtins fixtures/tuple.pyi]

[case testUninitializedAttributeTypedDict]
Expand Down Expand Up @@ -400,7 +408,7 @@ if TYPE_CHECKING:
class C(B): ...
else:
class C: ...
C() # E: Class "C" has annotated but unset attributes: "x"
C() # E: Class "C" has annotated but unset instance attributes: "x"
[file stub.pyi]
class A:
x: int
Expand Down Expand Up @@ -444,7 +452,7 @@ class C:
x: int
class D(C):
x: int
D() # E: Class "D" has annotated but unset attributes: "x"
D() # E: Class "D" has annotated but unset instance attributes: "x"

class E:
a: int
Expand Down

0 comments on commit 208e0a0

Please sign in to comment.