Repository navigation
New semantic analyzer: abstract classes #6423
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
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
aaf0b21
New semantic analyzer: fix abstractness calculations
JukkaL 2504f6b
Remove old
JukkaL f070233
Minor refactoring
JukkaL 0590641
Infer simple signatures of overloaded functions
JukkaL a208192
Remove old code
JukkaL 2b63e80
Only recurse into nested classes if defined in current module
JukkaL 12525c2
Clean up
JukkaL 4a85dfa
Update test case
JukkaL 4cc0612
Merge branch 'master' into semanal-abstract
JukkaL 9c0b913
Update based on feedback
JukkaL File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| """Calculate the abstract status of classes. | ||
|
|
||
| This happens after semantic analysis and before type checking. | ||
| """ | ||
|
|
||
| from typing import List, Set, Optional | ||
|
|
||
| from mypy.nodes import Node, MypyFile, SymbolTable, TypeInfo, Var, Decorator, OverloadedFuncDef | ||
| from mypy.errors import Errors | ||
|
|
||
|
|
||
| def calculate_abstract_status(file: MypyFile, errors: Errors) -> None: | ||
| """Calculate the abstract status of all classes in the symbol table in file. | ||
|
|
||
| Also check that ABCMeta is used correctly. | ||
| """ | ||
| process(file.names, file.is_stub, file.fullname(), errors) | ||
|
|
||
|
|
||
| def process(names: SymbolTable, is_stub_file: bool, prefix: str, errors: Errors) -> None: | ||
| for name, symnode in names.items(): | ||
| node = symnode.node | ||
| if isinstance(node, TypeInfo) and node.fullname().startswith(prefix): | ||
| calculate_class_abstract_status(node, is_stub_file, errors) | ||
| new_prefix = prefix + '.' + node.name() | ||
| process(node.names, is_stub_file, new_prefix, errors) | ||
|
|
||
|
|
||
| def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: Errors) -> None: | ||
| """Calculate abstract status of a class. | ||
|
|
||
| Set is_abstract of the type to True if the type has an unimplemented | ||
| abstract attribute. Also compute a list of abstract attributes. | ||
| Report error is required ABCMeta metaclass is missing. | ||
| """ | ||
| concrete = set() # type: Set[str] | ||
| abstract = [] # type: List[str] | ||
| abstract_in_this_class = [] # type: List[str] | ||
| for base in typ.mro: | ||
| for name, symnode in base.names.items(): | ||
| node = symnode.node | ||
| if isinstance(node, OverloadedFuncDef): | ||
| # Unwrap an overloaded function definition. We can just | ||
| # check arbitrarily the first overload item. If the | ||
| # different items have a different abstract status, there | ||
| # should be an error reported elsewhere. | ||
| func = node.items[0] # type: Optional[Node] | ||
| else: | ||
| func = node | ||
| if isinstance(func, Decorator): | ||
| fdef = func.func | ||
| if fdef.is_abstract and name not in concrete: | ||
| typ.is_abstract = True | ||
| abstract.append(name) | ||
| if base is typ: | ||
| abstract_in_this_class.append(name) | ||
| elif isinstance(node, Var): | ||
| if node.is_abstract_var and name not in concrete: | ||
| typ.is_abstract = True | ||
| abstract.append(name) | ||
| if base is typ: | ||
| abstract_in_this_class.append(name) | ||
| concrete.add(name) | ||
| # In stubs, abstract classes need to be explicitly marked because it is too | ||
| # easy to accidentally leave a concrete class abstract by forgetting to | ||
| # implement some methods. | ||
| typ.abstract_attributes = sorted(abstract) | ||
| if is_stub_file: | ||
| if typ.declared_metaclass and typ.declared_metaclass.type.fullname() == 'abc.ABCMeta': | ||
| return | ||
| if typ.is_protocol: | ||
| return | ||
| if abstract and not abstract_in_this_class: | ||
| def report(message: str, severity: str) -> None: | ||
| errors.report(typ.line, typ.column, message, severity=severity) | ||
|
|
||
| attrs = ", ".join('"{}"'.format(attr) for attr in sorted(abstract)) | ||
| report("Class {} has abstract attributes {}".format(typ.fullname(), attrs), 'error') | ||
| report("If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass", | ||
| 'note') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| """Simple type inference for decorated functions during semantic analysis.""" | ||
|
|
||
| from typing import Optional | ||
|
|
||
| from mypy.nodes import Expression, Decorator, CallExpr, FuncDef, RefExpr, Var, ARG_POS | ||
| from mypy.types import Type, CallableType, AnyType, TypeOfAny, TypeVarType, function_type | ||
| from mypy.typevars import has_no_typevars | ||
| from mypy.newsemanal.semanal_shared import SemanticAnalyzerInterface | ||
|
|
||
|
|
||
| def infer_decorator_signature_if_simple(dec: Decorator, | ||
| analyzer: SemanticAnalyzerInterface) -> None: | ||
| """Try to infer the type of the decorated function. | ||
|
|
||
| This lets us resolve additional references to decorated functions | ||
| during type checking. Otherwise the type might not be available | ||
| when we need it, since module top levels can't be deferred. | ||
|
|
||
| This basically uses a simple special-purpose type inference | ||
| engine just for decorators. | ||
| """ | ||
| if dec.var.is_property: | ||
| # Decorators are expected to have a callable type (it's a little odd). | ||
| if dec.func.type is None: | ||
| dec.var.type = CallableType( | ||
| [AnyType(TypeOfAny.special_form)], | ||
| [ARG_POS], | ||
| [None], | ||
| AnyType(TypeOfAny.special_form), | ||
| analyzer.named_type('__builtins__.function'), | ||
| name=dec.var.name()) | ||
| elif isinstance(dec.func.type, CallableType): | ||
| dec.var.type = dec.func.type | ||
| return | ||
| decorator_preserves_type = True | ||
| for expr in dec.decorators: | ||
| preserve_type = False | ||
| if isinstance(expr, RefExpr) and isinstance(expr.node, FuncDef): | ||
| if expr.node.type and is_identity_signature(expr.node.type): | ||
| preserve_type = True | ||
| if not preserve_type: | ||
| decorator_preserves_type = False | ||
| break | ||
| if decorator_preserves_type: | ||
| # No non-identity decorators left. We can trivially infer the type | ||
| # of the function here. | ||
| dec.var.type = function_type(dec.func, analyzer.named_type('__builtins__.function')) | ||
| if dec.decorators: | ||
| return_type = calculate_return_type(dec.decorators[0]) | ||
| if return_type and isinstance(return_type, AnyType): | ||
| # The outermost decorator will return Any so we know the type of the | ||
| # decorated function. | ||
| dec.var.type = AnyType(TypeOfAny.from_another_any, source_any=return_type) | ||
| sig = find_fixed_callable_return(dec.decorators[0]) | ||
| if sig: | ||
| # The outermost decorator always returns the same kind of function, | ||
| # so we know that this is the type of the decorated function. | ||
| orig_sig = function_type(dec.func, analyzer.named_type('__builtins__.function')) | ||
| sig.name = orig_sig.items()[0].name | ||
| dec.var.type = sig | ||
|
|
||
|
|
||
| def is_identity_signature(sig: Type) -> bool: | ||
| """Is type a callable of form T -> T (where T is a type variable)?""" | ||
| if isinstance(sig, CallableType) and sig.arg_kinds == [ARG_POS]: | ||
| if isinstance(sig.arg_types[0], TypeVarType) and isinstance(sig.ret_type, TypeVarType): | ||
| return sig.arg_types[0].id == sig.ret_type.id | ||
| return False | ||
|
|
||
|
|
||
| def calculate_return_type(expr: Expression) -> Optional[Type]: | ||
| """Return the return type if we can calculate it. | ||
|
|
||
| This only uses information available during semantic analysis so this | ||
| will sometimes return None because of insufficient information (as | ||
| type inference hasn't run yet). | ||
| """ | ||
| if isinstance(expr, RefExpr): | ||
| if isinstance(expr.node, FuncDef): | ||
| typ = expr.node.type | ||
| if typ is None: | ||
| # No signature -> default to Any. | ||
| return AnyType(TypeOfAny.unannotated) | ||
| # Explicit Any return? | ||
| if isinstance(typ, CallableType): | ||
| return typ.ret_type | ||
| return None | ||
| elif isinstance(expr.node, Var): | ||
| return expr.node.type | ||
| elif isinstance(expr, CallExpr): | ||
| return calculate_return_type(expr.callee) | ||
| return None | ||
|
|
||
|
|
||
| def find_fixed_callable_return(expr: Expression) -> Optional[CallableType]: | ||
| """Return the return type, if expression refers to a callable that returns a callable. | ||
|
|
||
| But only do this if the return type has no type variables. Return None otherwise. | ||
| This approximates things a lot as this is supposed to be called before type checking | ||
| when full type information is not available yet. | ||
| """ | ||
| if isinstance(expr, RefExpr): | ||
| if isinstance(expr.node, FuncDef): | ||
| typ = expr.node.type | ||
| if typ: | ||
| if isinstance(typ, CallableType) and has_no_typevars(typ.ret_type): | ||
| if isinstance(typ.ret_type, CallableType): | ||
| return typ.ret_type | ||
| elif isinstance(expr, CallExpr): | ||
| t = find_fixed_callable_return(expr.callee) | ||
| if t: | ||
| if isinstance(t.ret_type, CallableType): | ||
| return t.ret_type | ||
| return None | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Maybe add a docstring while you are at it?