Skip to content
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

Add new flag for warning about instantiating classes with declared but uninitialized attributes #13797

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

tmke8
Copy link
Contributor

@tmke8 tmke8 commented Oct 3, 2022

Fixes #4426
(also related: #4019)

Issue #4426 proposed prohibiting abstract attributes. I took a stab at implementing this, but I added it behind a new flag.
The new flag makes mypy complain when variables are declared in a class (with a type annotation) but never initialized. Other type checkers have similar features: for pyright, it's behind the flag reportUninitializedInstanceVariable and pyre does this by default.

One problem is that there are many ways to go about this. I think there are 4 natural places where the warning could be raised:

class A:  # option A: complain here
    x: int
    y: str

    def __init__(self) -> None:  # option B: complain here
        self.x = 0

a = A()  # option C: complain here
a.y  # option D: complain here

I went with option C because 1) it was easier to implement because I could re-use the code that deals with Protocol attributes; 2) it's more flexible than option A and B while easier to keep track of than option D; and 3) it fits with other reporting patterns in mypy, which is to report abstract status at the point of instantiation.

Option C is more flexible because the following code is allowed:

from abc import ABC
class A(ABC):  # (this would have worked without the ABC as well though)
    x: str
class B(A):
    def __init__(self) -> None:
        self.x = "foo"
B()  # OK

class C:
    x: int
    def f(self) -> None:  # note that this isn't __init__
        self.x = 0
C()  # also OK

Though I'll note that pyright and pyre went with option A (suppressing the error for abstract classes).

Because I repurposed the Protocol attribute code, the error messages aren't ideal yet, but I can still change that if the general idea of the PR is approved of. Currently, if --warn-uninitialized-attributes is turned on, then the error message for this code

class A:
    x: int
a = A()

is:

error: Cannot instantiate abstract class "A" with abstract attribute "x"

The name of the flag is --warn-uninitialized-attributes, but tell me if it should be something else.

By the way, if I turn on this flag for the self check in the tests, then this is the result (8 errors).
mypy/nodes.py:399: error: Cannot instantiate abstract class "MypyFile" with abstract attributes "_fullname" and "names"  [abstract]                                                                                            
            tree = MypyFile([], [])                                                                                                                                                                                            
                   ^~~~~~~~~~~~~~~~                                                                                                                                                                                            
mypy/treetransform.py:149: error: Cannot instantiate abstract class "MypyFile" with abstract attributes "_fullname" and "names"  [abstract]                                                                                    
            new = MypyFile(self.statements(node.defs), [], node.is_bom, ignored_lines=ignored_lines)                                                                                                                           
                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                                                                                                           
mypy/fastparse.py:303: error: Cannot instantiate abstract class "MypyFile" with abstract attributes "_fullname" and "names"  [abstract]                                                                                        
            tree = MypyFile([], [], False, {})                                                                                                                                                                                
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~                                                                                                                                                                                
mypy/fastparse.py:846: error: Cannot instantiate abstract class "MypyFile" with abstract attributes "_fullname" and "names"  [abstract]                                                                                        
            return MypyFile(body, self.imports, False, self.type_ignores)                                                                                                                                                      
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                                                                                                                                      
mypy/test/data.py:281: error: Only concrete class can be given where "Type[DataSuiteCollector]" is expected  [type-abstract]                                                                                                   
            parent = self.getparent(DataSuiteCollector)                                                                                                                                                                        
                                    ^~~~~~~~~~~~~~~~~~                                                                                                                                                                         
mypy/checker.py:376: error: Cannot instantiate abstract class "PatternChecker" with abstract attributes "subject" and "subject_type"  [abstract]                                                                              
            self.pattern_checker = PatternChecker(self, self.msg, self.plugin)                                                                                                                                                
                                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                                                                                                                                 
mypy/build.py:644: error: Cannot instantiate abstract class "SemanticAnalyzer" with abstract attributes "global_decls" and "nonlocal_decls"  [abstract]                                                                        
            self.semantic_analyzer = SemanticAnalyzer(                                                                                                                                                                        
                                     ^                                                                                                                                                                                         
mypy/stubtest.py:1749: error: Cannot instantiate abstract class "_Arguments" with abstract attributes "allowlist", "check_typeshed", ... and "version" (8 methods suppressed)  [abstract]                                      
        return parser.parse_args(args, namespace=_Arguments())                                                                                                                                                                
                                                 ^~~~~~~~~~~~                                                                                                                                                                  
Found 8 errors in 7 files (checked 167 source files)

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@tmke8
Copy link
Contributor Author

tmke8 commented Oct 6, 2022

I enabled the flag by default in order to look at the mypy_primer output.

The error in https://github.com/mystor/git-revise is from this pattern:

from __future__ import annotations
from typing import Type, cast

class A:
    x: int
    def __new__(cls: Type[A], x: int) -> A:
        self = super().__new__(cls)
        self.x = x
        return cast(A, self)

a = A(3)

I don't think this can be supported...

@tmke8
Copy link
Contributor Author

tmke8 commented Oct 6, 2022

https://github.com/encode/starlette has this:

class State:
    _state: typing.Dict[str, typing.Any]

    def __init__(self, state: typing.Optional[typing.Dict[str, typing.Any]] = None):
        if state is None:
            state = {}
        super().__setattr__("_state", state)

Also very difficult to support, I think.

@github-actions

This comment has been minimized.

@tmke8
Copy link
Contributor Author

tmke8 commented Oct 6, 2022

This pattern in https://github.com/systemd/mkosi

class A:
    CONST: int
    CONST = 4
a = A()

should work though (even though that should be a ClassVar).

I'll fix this.

@github-actions

This comment has been minimized.

Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is not a full review: just a bunch of ideas :)

# flags: --warn-uninitialized-attributes
class A:
x: int
a = A() # E: Cannot instantiate abstract class "A" with abstract attribute "x"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should totally change the error message.
Class A is not abstract by any means.

Maybe we should say: Cannot instantiate aclass "A" with annotated but unset attribute "x"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now went with Class "A" has annotated but unset attributes: "x" because "Cannot instantiate" seemed too strong to me (because the instantiation will work fine at runtime). But I'm not attached to this at all and I'm happy to change it again.

test-data/unit/check-warnings.test Outdated Show resolved Hide resolved
test-data/unit/check-warnings.test Show resolved Hide resolved
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@tmke8
Copy link
Contributor Author

tmke8 commented Nov 2, 2022

One question is whether initialization should only be recognized if it happens in the __init__. As the code is right now, any method in a class can first initialize a variable, but maybe that's too loose behavior for people who want to use the new flag.

Comment on lines 649 to 664
elif isinstance(sym.node, Var):
# NamedTuple fields are initialized in the generated __init__.
sym.node.is_uninitialized = False
pass
# 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 initialized in the generated __init__.
value.node.is_uninitialized = False
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly don't understand why I needed to add this check twice, but if I don't, then either the self-check or the tests fails.

elif isinstance(sym.node, Var):
# NamedTuple fields are initialized in the generated __init__.
sym.node.is_uninitialized = False
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe because of pass? Isn't it suppose to be continue?

@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@sobolevn
Copy link
Member

Please, resolve merge conflicts

@github-actions
Copy link
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Should we always prohibit "abstract" attributes?
2 participants