-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Disallow implicit Any types from Silent Imports #3405
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
These types can appear from an unanalyzed module. If mypy encounters a type annotation that uses such a type, it will report an error. Fixes python#3205
704a7b7
to
29848f8
Compare
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.
Did a quick review pass. Looks pretty good! We may want to spend a little more time thinking about the names of various Any
-related options, since it looks like we might have a few options that are pretty closely related and it's unclear how to distinguish the different options from each other in a way that is intuitive to users.
test-data/unit/check-flags.test
Outdated
from missing import MyType | ||
|
||
def f(x: MyType) -> None: # E: Argument 1 to 'f' is implicitly converted to 'Any' due to import from unanalyzed module | ||
pass |
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.
Use a 4-space indent here and elsewhere in tests (you can leave existing tests that use a 2-space indent unmodified).
test-data/unit/check-flags.test
Outdated
return l | ||
[builtins fixtures/list.pyi] | ||
[out] | ||
main:5: error: Return type is implicitly converted to 'builtins.list[Any]' due to import from unanalyzed module |
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.
Would it make sense to update the types to be formatted as List[Any]
etc. for consistency with most other messages? builtins.list[Any]
and such are pretty verbose and not how users write the types.
test-data/unit/check-flags.test
Outdated
class C(Unchecked): # E: Subclassing type 'Unchecked' that is implicitly converted to 'Any' due to import from unanalyzed module | ||
pass | ||
|
||
class A(List[Unchecked]): # E: Subclassing a type that is implicitly converted to 'builtins.list[Any]' due to import from unanalyzed module |
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.
Similar to above (List[Any]
preferred over builtins.list[Any]
).
test-data/unit/check-flags.test
Outdated
pass | ||
|
||
class A(List[Unchecked]): # E: Subclassing a type that is implicitly converted to 'builtins.list[Any]' due to import from unanalyzed module | ||
pass |
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.
Ideas for additional test cases:
- Type alias like
X = List[Unchecked]
orX = Unchecked
. cast(List[Unchecked], foo)
NamedTuple
uses an implicitAny
type.T = TypeVar('T', Unchecked, str)
T = TypeVar('T', bound=Unchecked)
X = NewType('X', Unchecked)
- Implicitly converted type within a callable type or a tuple type.
The code review feedback should be addressed now. |
FWIW, I think the name of the flag you propose is too generic, e.g. |
@ilevkivskyi Yes, I defintely agree with you. In #3427 I mentioned |
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.
I haven't had a chance to look closely at the tests yet, but here's a review of everything else.
Overall, nicely done! Most of my comments are relatively small tweaks. A couple things to note that didn't fit well elsewhere:
- This diff includes a typeshed change -- be careful about that! It's super easy to do so accidentally. I found the typeshed subrepo to be a pain to manage until I added the following as a
post-checkout
andpost-merge
git hook:
#!/bin/sh
git submodule update
After that, it's basically never been in my way. Highly recommended.
- In some comments and error messages, this code talks about types that are "implicitly converted to Any", which isn't quite right. The types aren't being converted, per se, it's more that we don't have any information about them, so they've fallen back to Any. The exact messaging around this (like the name of the flag) probably bears some more thought.
Finally, as a stylistic point, I'd recommend responding individually to all review comments so reviewers can know exactly which things you've addressed in your changes so far, which things you plan to address in future changes, and which things you disagree with. I often write these responses as I'm making the changes, which helps me keep track of things and make sure I'm not missing anything (and sometimes actually making the change provides important context for my response). On the review side, it's always super helpful to know which comments were actually addressed and not, as a "respond to review" commit can mean many things.
mypy/options.py
Outdated
@@ -158,3 +160,6 @@ def module_matches_pattern(self, module: str, pattern: Pattern[str]) -> bool: | |||
|
|||
def select_options_affecting_cache(self) -> Mapping[str, bool]: | |||
return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE} | |||
|
|||
def silent_mode(self) -> bool: |
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.
I'd consider silent_imports_mode
instead.
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.
Though actually I think one of the call sites will probably be removed, making this no longer necessary.
mypy/checker.py
Outdated
elif (self.options.disallow_implicit_any_types | ||
and has_any_from_silent_import(lvalue_type)): | ||
prefix = "Type of {}".format(lvalue_name) | ||
self.msg.implicit_any_from_silent_import(prefix, lvalue_type, context) |
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 gives an error on every line where a silent-import-Any-typed variable is assigned to, which is undesirable. We'd prefer to only give an error on the first assignment.
mypy/checkexpr.py
Outdated
self.msg.redundant_cast(target_type, expr) | ||
if options.disallow_implicit_any_types and has_any_from_silent_import(target_type): | ||
self.msg.implicit_any_from_silent_import("Target type of cast", target_type, expr) |
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 won't work properly if the variable is casted to a Callable
.
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.
I'm not totally sure what you mean. I tried the following code and it gave an error (like it's supposed to).
from typing import cast, Callable
from missing import Unchecked
foo = [1, 2, 3]
call = cast(Callable[[], Unchecked], foo)
Produces the following output:
m.py:6: error: Target type of cast is implicitly converted to Callable[[], Any] due to import from unanalyzed module
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.
Oh, I see what you're saying. Maybe it should saysomething along the lines of Return type is converted Any from Unchecked
(not exact wording). Is that what you mean?
I am actually not sure about this. In another case we are typechecking a function definition (where the user types in def foo ...
), so there is an error specific to return type and arguments. However, when the user types in Callable, I think talking about Callable as its own type makes sense. Do you have a strong opinion on this?
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.
Ah, that was my misunderstanding. I saw you dissecting Callable
types here and assumed that was because has_any_from_silent_import
didn't properly handle them. Taking a closer look, you're just trying to give people a better error message. That seems reasonable!
mypy/semanal.py
Outdated
if self.options.disallow_subclassing_any: | ||
# if --disallow-implicit-any-types is set, the issue is reported later | ||
implicit = self.options.disallow_implicit_any_types and base.is_from_silent_import | ||
if self.options.disallow_subclassing_any and not implicit: |
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.
I actually think that emitting both errors here is the correct thing to do.
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.
Yeah, I think it's okay to have both. I will remove the extra check.
As long as it's not overwhelming for the user, it's actually better to have more information than less. The new message would explain more. This is what it outputs now for a simple case:
error: Cannot find module named 'missing'
note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
error: Class cannot subclass 'Unchecked' (has type 'Any')
error: Base type Unchecked is implicitly converted to "Any" due to import from unanalyzed module
mypy/semanal.py
Outdated
@@ -964,7 +965,9 @@ def analyze_base_classes(self, defn: ClassDef) -> None: | |||
self.fail("Cannot subclass NewType", defn) | |||
base_types.append(base) | |||
elif isinstance(base, AnyType): | |||
if self.options.disallow_subclassing_any: | |||
# if --disallow-implicit-any-types is set, the issue is reported later | |||
implicit = self.options.disallow_implicit_any_types and base.is_from_silent_import |
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.
implicit
isn't a great name for this variable -- that sounds like something that should be only a property of base
, but in fact it also depends on an error-related option. I'd consider something like more explicit like disallowed_implicit_any
instead.
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.
Yeah, I'll remove this variable as per comment below.
mypy/semanal.py
Outdated
any_type = AnyType() | ||
if self.options.silent_mode(): | ||
# if silent mode is not enabled, no need to report implicit conversion to Any, | ||
# let mypy report an import error. |
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.
I think we should still report an errors in this instance, actually -- there's no point in making the flag only work with a very specific combination of other flags when a slightly more general use is perfectly valid. Even if the exact use-case is less clear, having consistent behavior is worthwhile.
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.
Yeah, this makes sense. It's not obvious that the new flag should be used with the silent imports.
mypy/typeanal.py
Outdated
@@ -232,7 +232,9 @@ def visit_unbound_type(self, t: UnboundType) -> Type: | |||
# context. This is slightly problematic as it allows using the type 'Any' | |||
# as a base class -- however, this will fail soon at runtime so the problem | |||
# is pretty minor. | |||
return AnyType() | |||
any_type = AnyType() | |||
any_type.is_from_silent_import = sym.node.type.is_from_silent_import |
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 suggests is_from_silent_import
to me that should be an optional argument to AnyType
's constructor.
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.
Yes, I was thinking about it. Would probably make it much cleaner.
What stopped me was that there is already an parameter implicit
in the constructor from this commit.
It should be fine if I comment it though.
mypy/typeanal.py
Outdated
@@ -731,6 +733,23 @@ def visit_callable_type(self, t: CallableType) -> TypeVarList: | |||
return [] | |||
|
|||
|
|||
def has_any_from_silent_import(t: Type) -> bool: |
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.
Rather than special-casing Callable
at some of the call sites, it'd be better to handle it here.
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.
Responded to this in this discussion: #3405 (comment)
Thanks for the feedback, David! Yeah, I realized I commited typeshed after I submitted the PR. It should be good now! Thanks for the advice. I responded to all your comments - and will follow it in the future. Thanks! For now I addressed all of the feedback, except for the |
Also, handle multiple declaration
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 is looking good. I had a few minor nitpicks, but after we figure out naming/messaging it should be good to go!
mypy/checker.py
Outdated
if (s.type is not None and | ||
self.options.disallow_implicit_any_types and | ||
has_any_from_silent_import(s.type)): | ||
if isinstance(s.lvalues[-1], TupleExpr): # is multiple |
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.
Nitpick: this comment should be a bit more explicit. In general, I'd err on the side of trying to be very very clear in comments, even if that feels a little verbose. In this case, I'd consider moving the comment to the next line and writing something like:
# This is a multiple assignment. Instead of figuring out which type is problematic, give a generic error message.
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.
Yep, that makes sense! Agree.
mypy/checkexpr.py
Outdated
self.msg.redundant_cast(target_type, expr) | ||
if options.disallow_implicit_any_types and has_any_from_silent_import(target_type): | ||
self.msg.implicit_any_from_silent_import("Target type of cast", target_type, expr) |
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.
Ah, that was my misunderstanding. I saw you dissecting Callable
types here and assumed that was because has_any_from_silent_import
didn't properly handle them. Taking a closer look, you're just trying to give people a better error message. That seems reasonable!
mypy/types.py
Outdated
@@ -272,9 +272,12 @@ def serialize(self) -> JsonDict: | |||
class AnyType(Type): | |||
"""The type 'Any'.""" | |||
|
|||
def __init__(self, implicit: bool = False, line: int = -1, column: int = -1) -> None: | |||
def __init__(self, implicit: bool = False, from_silent_import: bool = False, | |||
line: int = -1, column: int = -1) -> None: |
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.
Nitpick: if you have to wrap a function def line, put each argument on a line of its own. There's a lot of existing code in mypy which doesn't do this, but it's much easier to read if done this way, so we're trying to do this for new code.
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.
Seems reasonable!
mypy/types.py
Outdated
super().__init__(line, column) | ||
self.implicit = implicit | ||
self.implicit = implicit # Was this Any type was inferred without a type annotation? |
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.
Nitpick: probably better to just put this comment on the preceding line for consistency with the next member variable initalization.
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.
Yup!
mypy/types.py
Outdated
super().__init__(line, column) | ||
self.implicit = implicit | ||
self.implicit = implicit # Was this Any type was inferred without a type annotation? | ||
# Does this come from an unresolved import? See--disallow-implicit-any-types flag |
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.
Trivial: See--
-> See --
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.
Didn't notice when I was committing! :)
test-data/unit/check-flags.test
Outdated
def f(x: X) -> None: # E: Argument 1 to "f" is implicitly converted to List[Any] due to import from unanalyzed module | ||
pass | ||
[builtins fixtures/list.pyi] | ||
[case testDisallowImplicitAnyCast] |
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.
Trivial: missing blank line before this line. It's a lot harder to read the tests if there isn't a blank line before each new testcase.
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.
Yes! Updated my other tests too.
and wording in variable names and comments to reflect the name change.
I updated the commit to use the new option name and syntax (for more, see #3470). In the near future, this option will accept a list of parameters but for now it only accepts one ( |
mypy/messages.py
Outdated
@@ -855,6 +855,11 @@ def unsupported_type_type(self, item: Type, context: Context) -> None: | |||
def redundant_cast(self, typ: Type, context: Context) -> None: | |||
self.note('Redundant cast to {}'.format(self.format(typ)), context) | |||
|
|||
def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> None: | |||
self.fail("{} becomes {} due to an unfollowed import (such imports occur either " |
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.
I'm not sure it is good to have an error message that long.
We could remove the explanation in parens but I'm not sure that most users will understand what an unfollowed import
is otherwise.
@ddfisher what do you think?
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.
I agree -- this error message is too long. I also agree that it'll probably be somewhat confusing. I'd say: remove the parenthetical explanation for now, and we can consider some other mechanism of explaining to the user if we think it's a big enough problem. (But we can/should do that in another PR.) We could consider outputting one Note:
at the end if there are any of these errors explaining what an unfollowed import is.
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.
Yeah, a note sounds like a better option!
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 is looking good -- just a few things to fix up in the new code!
mypy/main.py
Outdated
current_options = raw_options.split(',') | ||
for option in current_options: | ||
if option not in valid_disallow_any_options: | ||
formatted_opts = ', '.join(map("'{}'".format, valid_disallow_any_options)) |
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.
Nit: strongly prefer list comprehensions to map (or filter) in Python. I'm also a fan of functional programming, but list comprehensions are considered more pythonic.
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.
I'd also consider calling this "formatted_valid_options" in order to disambiguate from the other "*_options" variables. I'd also write out "options" fully here -- it's a bit more verbose, but is more consistent with the rest of the variable names.
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.
Yep!
The reason I chose map
in that situation was that it was shorter than using a comprehension.
But it's probably better to use whatever is more pythonic :)
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.
If you're interested, talk to Guido about his views on map
vs list comprehensions. (Hint: they are strong views.)
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.
I will, thanks :)
mypy/main.py
Outdated
@@ -203,6 +204,16 @@ def add_invertible_flag(flag: str, | |||
strict_flag_names.append(flag) | |||
strict_flag_assignments.append((dest, not default)) | |||
|
|||
def disallow_any_argument_type(raw_options: str) -> List[str]: | |||
current_options = raw_options.split(',') |
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.
I'm not sure this is named quite right. "current_options" implies that the options will be changing over time, which doesn't really happen. Maybe consider just "options"?
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.
Good point. I was trying to make it clear that these options are the ones provided by user. I think just options
works too!
mypy/main.py
Outdated
@@ -179,6 +179,7 @@ def process_options(args: List[str], | |||
|
|||
strict_flag_names = [] # type: List[str] | |||
strict_flag_assignments = [] # type: List[Tuple[str, bool]] | |||
valid_disallow_any_options = {'unimported'} |
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.
Tiny nit: I think the "valid_" part of this name doesn't really add anything (and is very slightly misleading because it isn't only used in a checking-for-validity context: it's also displayed to the user).
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.
More importantly, this should be a list instead of a set, because it's displayed to our users in a couple places. This is important for two reasons: 1) We don't want a nondeterministic --help
output. 2) We're going to want to specify the options in a particular order that we think makes the most sense to read them in.
A set is canonically the "right" data structure to use here, because you're mainly checking for membership. However, it's so small that it doesn't matter (especially because it's not going to be run in a tight loop or anything).
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.
Yep, all that makes sense!
Yeah, I didn't think about the fact that the help output could be in different order -- that's a good point!
mypy/main.py
Outdated
for option in current_options: | ||
if option not in valid_disallow_any_options: | ||
formatted_opts = ', '.join(map("'{}'".format, valid_disallow_any_options)) | ||
message = "Unrecognized option '{}' (valid options are: {}).".format( |
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 line needs to mention that the option is related to --disallow-any
. Much less importantly, I'd also consider "Invalid" instead of "Unrecognized", as it seems slightly more accurate.
I'd consider something like "Invalid '--disallow-any' option ..."
.
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.
Oh wow, yeah good catch. I assumed when the error would be thrown, it would say the option name but I guess not.
Yep!
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's always good to run these things on the command-line yourself to make sure the output really looks as expected! (And sometimes the way you feel about phrasing, etc, can change when you see it in context.)
mypy/messages.py
Outdated
@@ -855,6 +855,11 @@ def unsupported_type_type(self, item: Type, context: Context) -> None: | |||
def redundant_cast(self, typ: Type, context: Context) -> None: | |||
self.note('Redundant cast to {}'.format(self.format(typ)), context) | |||
|
|||
def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> None: | |||
self.fail("{} becomes {} due to an unfollowed import (such imports occur either " |
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.
I agree -- this error message is too long. I also agree that it'll probably be somewhat confusing. I'd say: remove the parenthetical explanation for now, and we can consider some other mechanism of explaining to the user if we think it's a big enough problem. (But we can/should do that in another PR.) We could consider outputting one Note:
at the end if there are any of these errors explaining what an unfollowed import is.
mypy/main.py
Outdated
for option in options: | ||
if option not in disallow_any_options: | ||
formatted_valid_options = ', '.join( | ||
"'{}'".format(option) for option in disallow_any_options) |
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.
option
here shadows option
defined on line 209 -- you really don't want to do that. I think a one-letter o
would be fine as a name here (because the context you need to see what that means is all right on the same line).
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.
Hmm, good point.
Actually, variable options
on line 208 overshadows options
from outer scope (line 372).
Will fix!
mypy/main.py
Outdated
for option in current_options: | ||
if option not in valid_disallow_any_options: | ||
formatted_opts = ', '.join(map("'{}'".format, valid_disallow_any_options)) | ||
message = "Unrecognized option '{}' (valid options are: {}).".format( |
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's always good to run these things on the command-line yourself to make sure the output really looks as expected! (And sometimes the way you feel about phrasing, etc, can change when you see it in context.)
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 good now! There's just one final style nit that I think is important enough to address before merging (see inline comment).
Should be good now! |
Thanks! |
These types can appear from an unanalyzed module.
If mypy encounters a type annotation that uses such a type,
it will report an error.
Note that this PR is different from #3141 although the flags have similar names. Perhaps, we should figure out the proper names for all flags.