-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Fix 7190:__init_subclass__ is not type-checked #7452
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
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.
Thank you for PR! I like this approach. I left few minor comments, could you also please move the logic to a separate method? (Like e.g. check_protocol_variance()
below.)
mypy/checker.py
Outdated
@@ -1647,6 +1647,24 @@ def visit_class_def(self, defn: ClassDef) -> None: | |||
with self.scope.push_class(defn.info): | |||
self.accept(defn.defs) | |||
self.binder = old_binder | |||
for base in typ.mro[1:]: | |||
if base.name() != 'object' and base.defn.info: |
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.
Checking for base.defn.info
looks suspicious, at this stage it must be always present, if it isn't, it is a bug elsewhere. Is this actually needed?
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.
actually there are AssertionErrors during tests, but I didn't succeed to reproduce them myself..
|
||
class object: | ||
def __init__(self) -> None: pass | ||
def __init_subclass__(cls, **kwargs) -> None: 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.
This doesn't match typeshed, see https://github.com/python/typeshed/blob/master/stdlib/2and3/builtins.pyi. I would prefer to keep such subtle things in fixtures closer to the truth.
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.
Thanks for updates! I have few more comments, mostly just minor style tweak.
mypy/checker.py
Outdated
7 Child() | ||
|
||
Base.__init_subclass__(thing=5) is called at line 4. This is what we simulate here | ||
Child.__init_subclass__ is never called |
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.
Thanks for adding the docstring! Normally we use this style:
"""Short one line summary.
The rest of the description after empty line.
"""
The summary in your case may be Check that keywords in a class definition are valid arguments for __init_subclass__().
mypy/checker.py
Outdated
# 'object.__init_subclass__ is a dummy method with no arguments, always defined | ||
# there is no use to call it | ||
if base.name() != 'object' \ | ||
and base.defn.info: # there are "NOT_READY" instances |
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 dangerous. Could you please post examples of tests that failed and the traceback?
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 have pasted traceback below (quite long though)
mypy/checker.py
Outdated
# we skip the current class itself | ||
for base in typ.mro[1:]: | ||
# 'object.__init_subclass__ is a dummy method with no arguments, always defined | ||
# there is no use to call it |
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.
In some sense it still makes sense to check this, for example:
>>> class C(test=True): ...
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init_subclass__() takes no keyword arguments
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.
You are right. I made the changes accordingly
mypy/checker.py
Outdated
call_expr.line = defn.line | ||
call_expr.column = defn.column | ||
call_expr.end_line = defn.end_line | ||
self.expr_checker.accept(call_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.
Using accept()
may actually be dangerous. For example if the base class is not in scope (defined in another module and not imported in the current one), this may trigger Name not defined
or maybe even a crash. There are two possible solutions here:
- Point
name_expr.node
to the base classTypeInfo
before callingaccept()
- Use
check_call()
or similar methods
Please add a test for 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.
Hm sorry, I don't understand. I tried to produce a mypy runtime exception with 2 modules, one of which defines a Base, and the other one doesn't import that Base, and it only gave me a nice error Name 'Base' is not defined
. Which os OK I guess? this is what would happen at runtime anyway (NameError: name 'Base' is not defined
)
I added a test for that
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.
Which os OK I guess?
It is not what I wanted. I wanted something like this:
# file bases.py
class Base:
def __init_subclass__(cls, **kwargs) -> None: ...
class MidBase(Base):
...
# file main.py
from bases import MidBase
class Main(MidBase, test=False):
...
test-data/unit/check-classes.test
Outdated
cls.default_name = default_name | ||
return | ||
|
||
|
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 it is better to have only one empty line here and below.
Co-Authored-By: Ivan Levkivskyi <levkivskyi@gmail.com>
Co-Authored-By: Ivan Levkivskyi <levkivskyi@gmail.com>
Co-Authored-By: Ivan Levkivskyi <levkivskyi@gmail.com>
Co-Authored-By: Ivan Levkivskyi <levkivskyi@gmail.com>
Co-Authored-By: Ivan Levkivskyi <levkivskyi@gmail.com>
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 almost ready, here are some more comments.
mypy/checker.py
Outdated
for base in typ.mro[1:]: | ||
# there are "NOT_READY" instances | ||
# during the tests, so I filter them out... | ||
if base.defn.info: |
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 I have an idea why this may be missing, but anyway this is probably not important since you don't need this anyway, everywhere where you use base.defn.info
you can use just base
, this is the same object.
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.
understood!
mypy/checker.py
Outdated
# there are "NOT_READY" instances | ||
# during the tests, so I filter them out... | ||
if base.defn.info: | ||
for method_name, method_symbol_node in base.defn.info.names.items(): |
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.
You don't need this to be a cycle, just use method_symbol_node = base.names.get('__init_subclass__')
.
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.
Or even simpler (if you are not going to use method_symbol_node
):
if '__init_subclass__' not in base.names:
continue
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.
you are right, I have modified accordingly
mypy/checker.py
Outdated
call_expr.line = defn.line | ||
call_expr.column = defn.column | ||
call_expr.end_line = defn.end_line | ||
self.expr_checker.accept(call_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.
Which os OK I guess?
It is not what I wanted. I wanted something like this:
# file bases.py
class Base:
def __init_subclass__(cls, **kwargs) -> None: ...
class MidBase(Base):
...
# file main.py
from bases import MidBase
class Main(MidBase, test=False):
...
a6c5089
to
2281a86
Compare
2fc04f3
to
2281a86
Compare
@ilevkivskyi Maybe it's better if I open another PR, with a clean history? this one starts being messy.. i am sorry about that |
We squash commits anyway, so this doesn't really matter. I will take a look at this later today. |
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.
Thanks, looks good! I can merge this after you will consider few style comments and undo the typeshed pin move.
mypy/checker.py
Outdated
@@ -1699,6 +1700,51 @@ def visit_class_def(self, defn: ClassDef) -> None: | |||
if typ.is_protocol and typ.defn.type_vars: | |||
self.check_protocol_variance(defn) | |||
|
|||
def check_init_subclass(self, defn: ClassDef) -> None: | |||
""" | |||
Check that keywords in a class definition are valid arguments for __init_subclass__(). |
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.
Move this to the line above, right after quotes. Essentially we just follow https://www.python.org/dev/peps/pep-0257/
Co-Authored-By: Ivan Levkivskyi <levkivskyi@gmail.com>
Co-Authored-By: Ivan Levkivskyi <levkivskyi@gmail.com>
thank you very much for helping |
This PR seems to have broken typeshed tests: https://travis-ci.org/python/typeshed/jobs/588255463 Seems an easy enough fix, but I wonder why this passes in this repository. |
Fixes #7190