Skip to content

Option to disallow explicit Any types (--disallow-any=explicit) #3579

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 17 commits into from
Jul 1, 2017

Conversation

ilinum
Copy link
Collaborator

@ilinum ilinum commented Jun 21, 2017

Type aliases are a little tricky to deal with because they get "inlined".
The solution was to unmark Anys as explicit right after we check a type alias
for explicit Anys.

(--disallow-any=explicit)

Type aliases are a little tricky to deal with because they get "inlined".
The solution was to unmark Anys as explicit right after we check a type alias
for explicit Anys.
@ilinum ilinum changed the title Implement option to disallow explicit Any types Option to disallow explicit Any types (--disallow-any=explicit) Jun 21, 2017
# Conflicts:
#	mypy/main.py
#	mypy/semanal.py
Copy link
Collaborator

@ddfisher ddfisher left a comment

Choose a reason for hiding this comment

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

Largely looks good! Thanks for the the thorough testing! Have a few nits.

mypy/messages.py Outdated
@@ -872,6 +872,9 @@ def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> N
self.fail("{} becomes {} due to an unfollowed import".format(prefix, self.format(typ)),
ctx)

def explicit_any(self, location: str, ctx: Context) -> None:
self.fail('Explicit "Any" is not allowed in a {}'.format(location), ctx)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if this error would be clearer if it was just Explicit "Any" is not allowed.. This reads to me like it's implying that the Any is not allowed in that particular location, but in fact it's not allowed in any location.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, actually, I agree. I think I was trying to be too fancy when it wasn't necessary.

mypy/semanal.py Outdated
@@ -4310,3 +4334,21 @@ def find_fixed_callable_return(expr: Expression) -> Optional[CallableType]:
if isinstance(t.ret_type, CallableType):
return t.ret_type
return None


def unmark_any_as_explicit(t: Type) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this name (and that of the associated class) needs a bit more thought -- I don't think it's clear what it means outside of the context of this PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, naming things is hard - I went through several iterations of names trying to name this function.

How about unset_any_type_as_explicit? Alternatively, we could try to make the function more generic and add a bool parameter explicit. Something like:

def set_any_type_explicit_attribute(explicit: bool) -> None: 
    ...

line: int = -1,
column: int = -1) -> None:
super().__init__(line, column)
# Was this Any type was inferred without a type annotation?
self.implicit = implicit
# Does this come from an unfollowed import? See --disallow-any=unimported option
self.from_unimported_type = from_unimported_type
# Does this Any come from an explicit type annotation?
self.explicit = explicit
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this not always the opposite of implicit? If not, the specifics need to be made very clear in the comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, explicit is not always the opposite of implicit, which is confusing.

implicit refers to situations when Any was inferred because a variable or parameter was not annotated.
explicit refers to situations when there was an explicit Any annotation.

However, if this Any comes, for instance, from an unimported type, both explicit and implicit will be False.

It seems to me that implicit has a wrong name. It is currently used for --disallow-untyped-defs to see if this Any comes from a parameter or variable with no explicit type annotation.

I think instead of tweaking comments we should rename implicit. How do you feel about calling it from_unannotated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added comment on implicit to make it more clear.

mypy/semanal.py Outdated
t.accept(UnmarkAnyAsExplicit())


class UnmarkAnyAsExplicit(TypeQuery[None]):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a bit of an abuse of the TypeQuery class. None of its parent classes seem to be set up for doing this easily, but I'm not entirely sure how I feel about this overall. @JukkaL do you think this is a reasonable thing to do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I agree, this does not seem right.
Moreover, this mutates a type, which is also not great.

What I need is a TypeVisitor that visit all types within a type. Maybe I can refactor one from TypeQuery and then make TypeQuery inherit the new visitor or something?

[file m.py]
from typing import Any, List
v: Any = ''
w = [''] # type: List[Any]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: It's better to vary test cases along one axis at once. Here, you're switching from testing a type annotation to a type comment, but you're also switching from testing a plain Any to an Any in a generic class. (And also the list shouldn't need any contents.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All right, that makes a lot of sense.

Also, I will split these tests into two.


def foo(x: TupleAny[str]) -> None: # no error
pass

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you also check that TupleAny[Any] produces an error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Wow, this is an interesting edge case. Will add a test.

[file m.py]
from typing import Any, List, NewType

# The commented out line is covered without the flag enabled - it is tested elsewhere
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd instead be a bit more specific and say "making a NewType of Any is never allowed".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, that wording is definitely better.

However, I think it's better to include the test (Baz = NewType('Baz', Any)).
In a different PR Guido pointed out that tests should follow features, not implementation.

And I think in this case, it's better to have an extra tests. Because if this errors stops being reported --disallow-any=explicit will not work - it won't report an explicit Any.

So I am going to include the test here.

@ilinum
Copy link
Collaborator Author

ilinum commented Jun 22, 2017

Most feedback should be addressed now.

I also realized that the originally this PR did not report errors when Any was used in TypedDict declaration, so I added that.

@ilinum ilinum force-pushed the disallow-any=explicit branch from 4332307 to 3d87808 Compare June 22, 2017 20:00
mypy/semanal.py Outdated
t.accept(MarkAnyNonExplicit())


class MarkAnyNonExplicit(TypeQuery[None]):
Copy link
Collaborator Author

@ilinum ilinum Jun 28, 2017

Choose a reason for hiding this comment

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

@JukkaL need your advice on this.

As David pointed out, this use of TypeQuery seems like abuse of this class.
What do you think is a good way of dealing with this?

We could refactor a child of TypeQuery to have that. Or maybe there's another way.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What about having a TypeQuery that returns all Any types that are part of a type, and then iterate over the results of the query and set explicit? I.e. something like this:

for any_type in find_all_any_types(t):
    any_type.explicit = False

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm actually that might not be a good idea, since mutating types is only safe if you are certain that there is ever only a single reference to the type object. What about a type transform that replaces Any instances with a copy that has explicit set to False? You should be able to use TypeTranslator.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that's a really good idea!

Done!

@ilinum
Copy link
Collaborator Author

ilinum commented Jun 28, 2017

All feedback should be addressed now, except for using TypeQuery to set AnyType as non-explicit.

mypy/checker.py Outdated
@@ -627,6 +627,11 @@ def is_implicit_any(t: Type) -> bool:
if has_any_from_unimported_type(arg_type):
prefix = "Argument {} to \"{}\"".format(idx + 1, fdef.name())
self.msg.unimported_type_becomes_any(prefix, arg_type, fdef)
if ('explicit' in self.options.disallow_any and
Copy link
Collaborator

Choose a reason for hiding this comment

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

This same piece of code gets repeated almost identically many times. Can you refactor this into a function or a few functions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, done!

@ilinum
Copy link
Collaborator Author

ilinum commented Jun 29, 2017

All feedback is addressed now, this PR should be ready to be merged.

Copy link
Collaborator

@ddfisher ddfisher left a comment

Choose a reason for hiding this comment

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

LGTM!

@ilinum ilinum merged commit b7636cb into python:master Jul 1, 2017
@ilinum ilinum deleted the disallow-any=explicit branch July 1, 2017 04:03
@ilinum ilinum mentioned this pull request Jul 20, 2017
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.

3 participants