Skip to content

Improve message when function is missing return type #6773

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

Conversation

allisonking
Copy link
Contributor

Fixes #5952

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

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

Thank you! This looks good, but I have couple small suggestions.

mypy/checker.py Outdated
@@ -984,7 +984,12 @@ def is_unannotated_any(t: Type) -> bool:
check_incomplete_defs = self.options.disallow_incomplete_defs and has_explicit_annotation
if show_untyped and (self.options.disallow_untyped_defs or check_incomplete_defs):
if fdef.type is None and self.options.disallow_untyped_defs:
self.fail(message_registry.FUNCTION_TYPE_EXPECTED, fdef)
if (not len(fdef.arguments) or
Copy link
Member

Choose a reason for hiding this comment

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

You don'y need len(...) here, just not fdef.arguments.

mypy/checker.py Outdated
@@ -984,7 +984,12 @@ def is_unannotated_any(t: Type) -> bool:
check_incomplete_defs = self.options.disallow_incomplete_defs and has_explicit_annotation
if show_untyped and (self.options.disallow_untyped_defs or check_incomplete_defs):
if fdef.type is None and self.options.disallow_untyped_defs:
self.fail(message_registry.FUNCTION_TYPE_EXPECTED, fdef)
if (not len(fdef.arguments) or
(len(fdef.arg_names) == 1 and 'self' in fdef.arg_names)):
Copy link
Member

Choose a reason for hiding this comment

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

fdef.arg_names[0] == 'self' would be more straightforward. Also maybe you should check for both self and cls.

mypy/checker.py Outdated
if (not len(fdef.arguments) or
(len(fdef.arg_names) == 1 and 'self' in fdef.arg_names)):
self.fail(message_registry.RETURN_TYPE_EXPECTED_DISALLOW_UNTYPED, fdef)
self.note('Use "-> None" if function does not return a value', fdef)
Copy link
Member

Choose a reason for hiding this comment

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

I would only show this note if the function body doesn't have non-trivial return statements (i.e. return x not just plain return).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay sounds good-- could you point me to how to find if there's a return value?

Copy link
Member

Choose a reason for hiding this comment

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

You probably have to create a new StatementVisitor that traverses fdef.body looking for any ReturnStmt instances whose expr is not None.

(And don't take my word for it. :-)

Copy link
Member

@ilevkivskyi ilevkivskyi May 6, 2019

Choose a reason for hiding this comment

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

You can use stubgen.has_return_statement() (Maybe also move it to some common file like nodes.py?)

Copy link
Member

Choose a reason for hiding this comment

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

(Fix: srubgen -> stubgen)

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 took a stab at implementing this in nodes.py... let me know if it's reasonable! 😬

@@ -74,6 +74,8 @@
FUNCTION_TYPE_EXPECTED = "Function is missing a type annotation" # type: Final
ONLY_CLASS_APPLICATION = "Type application is only supported for generic classes" # type: Final
RETURN_TYPE_EXPECTED = "Function is missing a return type annotation" # type: Final
RETURN_TYPE_EXPECTED_DISALLOW_UNTYPED = \
"Function must have a return type annotation ('disallow_untyped_defs' enabled)" # type: Final
Copy link
Member

Choose a reason for hiding this comment

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

Although it is mentioned in the issue, I would not mention the name of option of the flag in the error message. It may easily get out of sync. So I would remove the part in (...).

mypy/checker.py Outdated
@@ -984,7 +984,12 @@ def is_unannotated_any(t: Type) -> bool:
check_incomplete_defs = self.options.disallow_incomplete_defs and has_explicit_annotation
if show_untyped and (self.options.disallow_untyped_defs or check_incomplete_defs):
if fdef.type is None and self.options.disallow_untyped_defs:
self.fail(message_registry.FUNCTION_TYPE_EXPECTED, fdef)
if (not len(fdef.arguments) or
(len(fdef.arg_names) == 1 and 'self' in fdef.arg_names)):
Copy link
Member

Choose a reason for hiding this comment

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

@JukkaL or @ilevkivskyi Is there a more reliable way to tell whether something's a method? Some (unusual) code bases use a different name instead of self, and class methods also aren't covered by this test.

It's also possible to scan the body to see whether occurrences of return have a value or not on them (though I don't know how practical that is from this context).

Copy link
Member

Choose a reason for hiding this comment

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

I think for self there is actually one: Var.is_self. I am not sure we have the same for cls.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I tried checking for fdef.arguments[0].variable.is_self here but it seems like I always get False. I couldn't find where is_self was being set-- for now I have this checking for the strings self and cls but agree it's not great

Copy link
Member

Choose a reason for hiding this comment

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

It may be because if a function is missing type annotation, then we don't analyze it at all during semantic analysis. I think it is OK to rely on the argument name, since this is needed only to tweak the error message, and not to decide whether we need to emit the error.

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

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

Thanks for updates! This is almost ready, I have few more comments.

mypy/checker.py Outdated
@@ -984,7 +984,14 @@ def is_unannotated_any(t: Type) -> bool:
check_incomplete_defs = self.options.disallow_incomplete_defs and has_explicit_annotation
if show_untyped and (self.options.disallow_untyped_defs or check_incomplete_defs):
if fdef.type is None and self.options.disallow_untyped_defs:
self.fail(message_registry.FUNCTION_TYPE_EXPECTED, fdef)
if (not fdef.arguments or (len(fdef.arguments) == 1 and
#(fdef.arguments[0].variable.is_self or fdef.arg_names[0] == 'cls'))):
Copy link
Member

Choose a reason for hiding this comment

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

This comment is not needed.

mypy/nodes.py Outdated
@@ -1101,6 +1111,19 @@ def __init__(self, expr: Optional[Expression]) -> None:
def accept(self, visitor: StatementVisitor[T]) -> T:
return visitor.visit_return_stmt(self)

class ReturnSeeker(StatementVisitor):
Copy link
Member

Choose a reason for hiding this comment

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

StatementVisitor[None]

mypy/nodes.py Outdated
def visit_return_stmt(self, o: ReturnStmt) -> None:
if (o.expr is None or isinstance(o.expr, NameExpr) and o.expr.name == 'None'):
return
self.found = True
Copy link
Member

Choose a reason for hiding this comment

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

This causes a bunch of code duplication with stubgen.py. Why don't you remove this class (and corresponding helper) from there and also use the added method there?

@@ -1063,7 +1063,8 @@ disallow_any_generics = True
def get_tasks(self):
return 'whatever'
[out]
a.py:1: error: Function is missing a type annotation
a.py:1: error: Function is missing a return type annotation
a.py:1: note: Use "-> None" if function does not return a value
Copy link
Member

Choose a reason for hiding this comment

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

This should not be here because method returns a string (see CI failures).

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

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

Thank you! I have just two more comments.

mypy/nodes.py Outdated
pass

def visit_exec_stmt(self, o: ExecStmt) -> None:
pass
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need all these dummy methods? Maybe instead just inherit from mypy.traverser.TraverserVisitor?

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 was having trouble importing the mypy.traverser.TraverserVisitor class, I believe because traverser.py also imports from nodes.py?

mypy/test/testcheck.py:9: in <module>
    from mypy import build
mypy/build.py:36: in <module>
    from mypy.nodes import MypyFile, ImportBase, Import, ImportFrom, ImportAll, SymbolTable
mypy/nodes.py:20: in <module>
    from mypy.traverser import TraverserVisitor
mypy/traverser.py:4: in <module>
    from mypy.nodes import (
E   ImportError: cannot import name 'Block' from 'mypy.nodes' (mypy/nodes.py)

Copy link
Member

Choose a reason for hiding this comment

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

Hm, yes, this would cause an import cycle. What if you instead keep this a function (not a method) and move it (together with the visitor class) to mypy.traverser and then import it from this common place in both cases (in stubgen.py and in checker.py)? I think this would be a reasonable solution.

mypy/stubgen.py Outdated
@@ -97,6 +97,7 @@ class Options:

This class is mutable to simplify testing.
"""

Copy link
Member

Choose a reason for hiding this comment

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

This empty line and a bunch below look unrelated to the PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

whoops-- turned off my autoformatter :)

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

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

Thank you for all the work! This looks ready now.

@ilevkivskyi ilevkivskyi merged commit 9c74efd into python:master May 9, 2019
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.

Improve message when function is missing return type
3 participants