Repository navigation
Make error messages from multiple inheritance compatibility check more accurate - #5926
Conversation
|
This indeed solves the issue I had in #5914, over 50 fewer false positives in the output now. Thanks! |
ilevkivskyi
left a comment
There was a problem hiding this comment.
Thanks for a PR!
Here are few comments.
| if second_type is None and isinstance(second.node, FuncBase): | ||
| second_type = self.function_type(second.node) | ||
| if isinstance(first.node, TypeInfo) and isinstance(second.node, TypeInfo): | ||
| # allow nested classes with the same name |
There was a problem hiding this comment.
In general I agree it is better to admit this little unsafety and fix a common pain point.
Please add a more detailed comment with motivation/trade-offs. Here are some points to mention:
- This is technically unsafe due to possible errors when accessing members of the nested classes.
- But this creates many false positives for Django.
- Also mypy anyway doesn't flag this for single inheritance.
- A potential solution may be to require at least structural compatibility (since requiring nominal one is impractical).
52284b1 to
9fe8046
Compare
|
@ilevkivskyi I addressed your comments. |
f4b055c to
c039095
Compare
|
When checking the test case from #5914, I get one error message reported when using c039095, while it is zero when using its parent commit 9fe8046. (For comparison, mypy master reports two errors.) So for my use case, the latest changes are a regression compared to the earlier proposed solution. Edit: Looking at it in more detail, the error message is new and not a remaining old message, so it's not really a regression. However, it does mean mypy is rejecting the situation where an inner class from one parent overrides an inner class from another parent, which may be too strict for the default settings. |
|
It looks like the Windows test failure is a flake. I am looking into it. |
There was a problem hiding this comment.
OK, so it looks like you changed your mind a bit. IIUC you want to allow only type safe (actually or forced by an Any base) overrides. I like this, but please consider that class Mixin(Any): ... actually fails with TypeError at runtime. This only works in stubs, at runtime one needs to do something like Unsafe: Any = object; class Mixin(Unsafe): ..., or are you going to add fallback_to_any by the plugin for all classes nested in models?
@mthuurne The current version of the PR actually type safe. I would say it makes sense to special-case this for Django (you will need to use the mypy plugin @mkurnikov is writing) instead of making this pattern non-type safe everywhere.
| if ((isinstance(first_type, CallableType) | ||
| and first_type.fallback.type.fullname() == 'builtins.type') | ||
| and (isinstance(second_type, CallableType) | ||
| and second_type.fallback.type.fullname() == 'builtins.type')): |
There was a problem hiding this comment.
This check is unreliable:
- What if the class constructor is overloaded?
- What if there is a custom metaclass?
Maybe you are trying to reinvent FunctionLike.is_type_obj()?
| # Both members are classes (not necessary nested), check if compatible | ||
| ok = is_subtype(first_type.ret_type, second_type.ret_type) | ||
| else: | ||
| # Method override |
There was a problem hiding this comment.
This comment adds nothing here, I would just remove it.
| and first_type.fallback.type.fullname() == 'builtins.type') | ||
| and (isinstance(second_type, CallableType) | ||
| and second_type.fallback.type.fullname() == 'builtins.type')): | ||
| # Both members are classes (not necessary nested), check if compatible |
There was a problem hiding this comment.
This comment is not clear. Probably you want to say something like: # For class objects only check the subtype relationship of the classes, since we allow incompatible overrides of '__init__'/'__new__'.
| and (isinstance(second_type, CallableType) | ||
| and second_type.fallback.type.fullname() == 'builtins.type')): | ||
| # Both members are classes (not necessary nested), check if compatible | ||
| ok = is_subtype(first_type.ret_type, second_type.ret_type) |
There was a problem hiding this comment.
This may cause surprises if the classes are generic, so the return types contain some (independent) type variables. I think you can just erase the type variables here (erasetype.erase_type_vars IIRC).
| pass | ||
| class A(Mixin1, Mixin2): | ||
| pass | ||
| [out] No newline at end of file |
There was a problem hiding this comment.
No newline at the end of file.
Yes, I tried it, it worked. In the wild I've encountered only nested classes used to:
Both of them are handled by the framework in very specific way, I wouldn't expect mypy to understand them without plugin, and errors are more or less OK there. |
c039095 to
1b41df3
Compare
|
Also please don't force push, GitHub doesn't send e-mail notifications for this either. I will take a look at the updates tomorrow. |
ilevkivskyi
left a comment
There was a problem hiding this comment.
Thanks! This is almost ready, I have just two more comments.
| # For class objects only check the subtype relationship of the classes, | ||
| # since we allow incompatible overrides of '__init__'/'__new__' | ||
| ok = is_subtype(left=fill_typevars(first_type.type_object()), | ||
| right=fill_typevars(second_type.type_object())) |
There was a problem hiding this comment.
I think using fill_typevars here is dangerous, since it may produce some false positives for generic classes (it creates an instance with type variable types). You need to apply erase_typevars. Or even better, add a helper function to typevars.py that will be very similar to fill_typevars, but would insert correct number of Any as type arguments.
ilevkivskyi
left a comment
There was a problem hiding this comment.
Thanks! I have few more small comments.
| T = TypeVar('T') | ||
| class ParentGeneric(Generic[T]): | ||
| pass | ||
| class ChildGeneric(ParentGeneric, Generic[T]): |
There was a problem hiding this comment.
Sorry, but this is still not good. You should rather write (here and below):
class ChildGeneric(ParentGeneric[T]):
passOtherwise the subclass maps to ParentGeneric[Any]. You can read some info about defining generic classes in the mypy docs.
There was a problem hiding this comment.
No problem, I feel like I need to read pep484 and docs end to end once more, I take too much of your time. I'll fix everything till the end of the day.
There was a problem hiding this comment.
I take too much of your time
Don't worry about this :-)
There was a problem hiding this comment.
Thanks!=)
Sorry, haven't found time, but I'm sure I will in a day or two.
| return typ.tuple_type.copy_modified(fallback=inst) | ||
|
|
||
|
|
||
| def fill_typevars_with_any(typ: TypeInfo) -> Union[Instance, TupleType]: |
There was a problem hiding this comment.
Could you please add a docstring here?
| def __new__(cls, *args, **kwargs: None) -> Mixin1.Meta: | ||
| pass | ||
| @overload | ||
| def __new__(cls, *args, **kwargs: Dict[str, Any]) -> Mixin1.Meta: |
There was a problem hiding this comment.
This is irrelevant for this PR (IIUC you are just testing that the overloaded constructor actually works), but this signature looks suspicious. If you have def func(**kwds: int): ..., then this call is valid func(x=1, y=2). From your signature it looks like every keyword arguments to the constructor are either all None, or all of them are dictionaries: Mixin1(x=None, y=None), or Mixin1(x={'a': 42, 'b': None}).
No need to fix this, just wanted to avoid confusions.
|
@mkurnikov What is the situation here? Is any input from my side needed? |
Fixes #2871.
Initially, in discussion with @ilevkivskyi in Gitter, he suggested to just remove error, if there are two nested classes in a multiple inheritance with the same name
However, later we decided to make it safe and emit a better error message, including for cases with nested class and non-class for obvious cases. Note that for class objects we ignore the
__init__method signature of nested class and only check subclassing relationship between them.