-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Implement ClassVar semantics (fixes #2771) #2873
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
Changes from 12 commits
d6b370d
950b383
cdfdbbc
01a7a69
d52b4e3
15e1e85
781245f
cd61a5d
9c29911
b618718
cb6786f
a427d61
35e2c68
e121158
5e2aafc
137215e
5dd131a
30a4185
bcce34d
7ddca4b
92b3532
20b1abe
3362e65
5430b32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1306,30 +1306,19 @@ def visit_block_maybe(self, b: Block) -> None: | |
def anal_type(self, t: Type, allow_tuple_literal: bool = False, | ||
aliasing: bool = False) -> Type: | ||
if t: | ||
if allow_tuple_literal: | ||
# Types such as (t1, t2, ...) only allowed in assignment statements. They'll | ||
# generate errors elsewhere, and Tuple[t1, t2, ...] must be used instead. | ||
if isinstance(t, TupleType): | ||
# Unlike TypeAnalyser, also allow implicit tuple types (without Tuple[...]). | ||
star_count = sum(1 for item in t.items if isinstance(item, StarType)) | ||
if star_count > 1: | ||
self.fail('At most one star type allowed in a tuple', t) | ||
return TupleType([AnyType() for _ in t.items], | ||
self.builtin_type('builtins.tuple'), t.line) | ||
items = [self.anal_type(item, True) | ||
for item in t.items] | ||
return TupleType(items, self.builtin_type('builtins.tuple'), t.line) | ||
a = TypeAnalyser(self.lookup_qualified, | ||
self.lookup_fully_qualified, | ||
self.fail, | ||
aliasing=aliasing) | ||
aliasing=aliasing, | ||
allow_tuple_literal=allow_tuple_literal) | ||
return t.accept(a) | ||
else: | ||
return None | ||
|
||
def visit_assignment_stmt(self, s: AssignmentStmt) -> None: | ||
for lval in s.lvalues: | ||
self.analyze_lvalue(lval, explicit_type=s.type is not None) | ||
self.check_classvar(s) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this change really necessary? I am asking for two reasons:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad. I've restored old behaviour, although in
Since I've removed the test for this we could allow this, but these variables wouldn't get |
||
s.rvalue.accept(self) | ||
if s.type: | ||
allow_tuple_literal = isinstance(s.lvalues[-1], (TupleExpr, ListExpr)) | ||
|
@@ -2159,6 +2148,29 @@ def build_typeddict_typeinfo(self, name: str, items: List[str], | |
|
||
return info | ||
|
||
def check_classvar(self, s: AssignmentStmt) -> None: | ||
lvalue = s.lvalues[0] | ||
if len(s.lvalues) != 1 or not isinstance(lvalue, (NameExpr, MemberExpr)): | ||
|
||
return | ||
if not self.check_classvar_definition(s.type): | ||
return | ||
if self.is_class_scope() and isinstance(lvalue, NameExpr): | ||
node = lvalue.node | ||
if isinstance(node, Var): | ||
node.is_classvar = True | ||
elif not isinstance(lvalue, MemberExpr) or self.is_self_member_ref(lvalue): | ||
# In case of member access, report error only when assigning to self | ||
# Other kinds of member assignments should be already reported | ||
self.fail('Invalid ClassVar definition', lvalue) | ||
|
||
def check_classvar_definition(self, typ: Type) -> bool: | ||
if not isinstance(typ, UnboundType): | ||
return False | ||
sym = self.lookup_qualified(typ.name, typ) | ||
if not sym or not sym.node: | ||
return False | ||
return sym.node.fullname() == 'typing.ClassVar' | ||
|
||
def visit_decorator(self, dec: Decorator) -> None: | ||
for d in dec.decorators: | ||
d.accept(self) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,11 +81,15 @@ def __init__(self, | |
lookup_func: Callable[[str, Context], SymbolTableNode], | ||
lookup_fqn_func: Callable[[str], SymbolTableNode], | ||
fail_func: Callable[[str, Context], None], *, | ||
aliasing: bool = False) -> None: | ||
aliasing: bool = False, | ||
allow_tuple_literal: bool = False) -> None: | ||
self.lookup = lookup_func | ||
self.lookup_fqn_func = lookup_fqn_func | ||
self.fail = fail_func | ||
self.aliasing = aliasing | ||
self.allow_tuple_literal = allow_tuple_literal | ||
# Positive if we are analyzing arguments of another (outer) type | ||
self.nesting_level = 0 | ||
|
||
def visit_unbound_type(self, t: UnboundType) -> Type: | ||
if t.optional: | ||
|
@@ -120,7 +124,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: | |
return self.builtin_type('builtins.tuple') | ||
if len(t.args) == 2 and isinstance(t.args[1], EllipsisType): | ||
# Tuple[T, ...] (uniform, variable-length tuple) | ||
instance = self.builtin_type('builtins.tuple', [t.args[0].accept(self)]) | ||
instance = self.builtin_type('builtins.tuple', [self.anal_nested(t.args[0])]) | ||
instance.line = t.line | ||
return instance | ||
return self.tuple_type(self.anal_array(t.args)) | ||
|
@@ -148,6 +152,16 @@ def visit_unbound_type(self, t: UnboundType) -> Type: | |
items = self.anal_array(t.args) | ||
item = items[0] | ||
return TypeType(item, line=t.line) | ||
elif fullname == 'typing.ClassVar': | ||
if self.nesting_level > 0: | ||
self.fail('Invalid ClassVar definition', t) | ||
if len(t.args) == 0: | ||
return AnyType(line=t.line) | ||
if len(t.args) != 1: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a bare |
||
self.fail('ClassVar[...] must have at most one type argument', t) | ||
return AnyType() | ||
items = self.anal_array(t.args) | ||
|
||
return items[0] | ||
elif fullname == 'mypy_extensions.NoReturn': | ||
return UninhabitedType(is_noreturn=True) | ||
elif sym.kind == TYPE_ALIAS: | ||
|
@@ -291,12 +305,14 @@ def visit_type_var(self, t: TypeVarType) -> Type: | |
|
||
def visit_callable_type(self, t: CallableType) -> Type: | ||
return t.copy_modified(arg_types=self.anal_array(t.arg_types), | ||
ret_type=t.ret_type.accept(self), | ||
ret_type=self.anal_nested(t.ret_type), | ||
fallback=t.fallback or self.builtin_type('builtins.function'), | ||
variables=self.anal_var_defs(t.variables)) | ||
|
||
def visit_tuple_type(self, t: TupleType) -> Type: | ||
if t.implicit: | ||
# Types such as (t1, t2, ...) only allowed in assignment statements. They'll | ||
# generate errors elsewhere, and Tuple[t1, t2, ...] must be used instead. | ||
if t.implicit and not self.allow_tuple_literal: | ||
self.fail('Invalid tuple literal type', t) | ||
return AnyType() | ||
star_count = sum(1 for item in t.items if isinstance(item, StarType)) | ||
|
@@ -308,13 +324,13 @@ def visit_tuple_type(self, t: TupleType) -> Type: | |
|
||
def visit_typeddict_type(self, t: TypedDictType) -> Type: | ||
items = OrderedDict([ | ||
(item_name, item_type.accept(self)) | ||
(item_name, self.anal_nested(item_type)) | ||
for (item_name, item_type) in t.items.items() | ||
]) | ||
return TypedDictType(items, t.fallback) | ||
|
||
def visit_star_type(self, t: StarType) -> Type: | ||
return StarType(t.type.accept(self), t.line) | ||
return StarType(self.anal_nested(t.type), t.line) | ||
|
||
def visit_union_type(self, t: UnionType) -> Type: | ||
return UnionType(self.anal_array(t.items), t.line) | ||
|
@@ -327,7 +343,7 @@ def visit_ellipsis_type(self, t: EllipsisType) -> Type: | |
return AnyType() | ||
|
||
def visit_type_type(self, t: TypeType) -> Type: | ||
return TypeType(t.item.accept(self), line=t.line) | ||
return TypeType(self.anal_nested(t.item), line=t.line) | ||
|
||
def analyze_callable_type(self, t: UnboundType) -> Type: | ||
fallback = self.builtin_type('builtins.function') | ||
|
@@ -340,7 +356,7 @@ def analyze_callable_type(self, t: UnboundType) -> Type: | |
fallback=fallback, | ||
is_ellipsis_args=True) | ||
elif len(t.args) == 2: | ||
ret_type = t.args[1].accept(self) | ||
ret_type = self.anal_nested(t.args[1]) | ||
if isinstance(t.args[0], TypeList): | ||
# Callable[[ARG, ...], RET] (ordinary callable type) | ||
args = t.args[0].items | ||
|
@@ -367,9 +383,16 @@ def analyze_callable_type(self, t: UnboundType) -> Type: | |
def anal_array(self, a: List[Type]) -> List[Type]: | ||
res = [] # type: List[Type] | ||
for t in a: | ||
res.append(t.accept(self)) | ||
res.append(self.anal_nested(t)) | ||
return res | ||
|
||
def anal_nested(self, t: Type) -> Type: | ||
self.nesting_level += 1 | ||
try: | ||
return t.accept(self) | ||
finally: | ||
self.nesting_level -= 1 | ||
|
||
def anal_var_defs(self, var_defs: List[TypeVarDef]) -> List[TypeVarDef]: | ||
a = [] # type: List[TypeVarDef] | ||
for vd in var_defs: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks surprisingly simple, but maybe more corner cases will be necessary for more tests, see below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does, but I think it works. There are tests with subclasses, callables,
Type
type and everything passes.