-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
A way to prevent MyPy from implictly checking the __r<magic method>__ #18945
New issue
Have a question about this project? No Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “No 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? No Sign in to your account
Comments
This sounds like you are saying mypy incorrectly typechecks operators like |
Yeah, messing with it more I think I may have discovered a bug. from mypy.types import LiteralType
from mypy.plugin import Plugin
def type_add(ctx, *args):
lhs = ctx.type
rhs = ctx.arg_types[0][0]
print(lhs)
print(rhs)
ret = lhs.value + rhs.value
# ctx.api.fail("test", ctx.context)
return LiteralType(ret, fallback=ctx.api.named_generic_type('builtins.int', []))
TYPE_DICT = {
'builtins.int.__add__': type_add
}
class TestPlugin(Plugin):
def get_method_hook(self, fullname):
print(fullname)
return TYPE_DICT.get(fullname, None)
def plugin(version: str):
return TestPlugin The input is essentially 3 + 4 When the
When the
From what I can tell, the flipping part is intended (and I would like to disable it) but I am not sure why it is checking it an extra time. If you would like to see the full code, it is right here: https://github.com/arnav-jain1/mypy_bug |
I would recommend checking the stack trace! IIRC there's a thing where if an expression fails we check whether it would work in an |
While that does make sense, it is still getting called with the lhs and rhs flipped, which I would like to prevent or ignore. Is there any way to do so? |
Happens here: Lines 4097 to 4114 in c274971
If the plugin produces any errors, we discard that variant and go try L4099 is definitely a bug: |
And the logic of checking Since there's no standard way to statically denote "this op method is non-cooperative and raises for unsupported arguments" as of now, The fix is as easy as diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index e7c2cba3f..9308164de 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -4096,7 +4096,7 @@ class ExpressionChecker(ExpressionVisitor[Type], ExpressionCheckerSharedApi):
results = []
for name, method, obj, arg in variants:
with self.msg.filter_errors(save_filtered_errors=True) as local_errors:
- result = self.check_method_call(op_name, obj, method, [arg], [ARG_POS], context)
+ result = self.check_method_call(name, obj, method, [arg], [ARG_POS], context)
if local_errors.has_new_errors():
errors.append(local_errors.filtered_errors())
results.append(result) and a test may involve a custom plugin (see |
Gotcha thank you! I will take a look and submit a PR |
I made the change but when I was testing it against with this plugin (previous Github repo has this too) I had a question from mypy.types import LiteralType
from mypy.plugin import Plugin
def type_add(ctx, *args):
lhs = ctx.type
rhs = ctx.arg_types[0][0]
print(lhs, rhs)
ret = lhs.value + rhs.value
ctx.api.fail("test", ctx.context)
return LiteralType(ret, fallback=ctx.api.named_generic_type('builtins.int', []))
class TestPlugin(Plugin):
def get_method_hook(self, fullname):
if fullname == 'builtins.int.__add__':
return type_add
return None
def plugin(version: str):
return TestPlugin When running it with the change, the output is
add is called twice and then radd is called so it is differentiating it but since there is no method for radd, there is no error so it treats it as no error. Should I leave it like this and require the plugin creator to have a radd method along with an add method (could potentially break old code, don't know though) or should I change the code so that add and radd are handled separately and if add has an error it will return the error (this I might need guidance on if there is an "elegant" non hacky way to do it) or any other way that you guys suggest. |
Ohh. No, please don't change this existing logic, it's sound - an error in one method is now consistently interpreted as a sign that the next one should be tried instead. There's no elegant way to only filter plugin-generated errors AFAIC. This level of breaking compat is acceptable IMO (most likely there's no single plugin in wild checking there dunders, or we'd have received a report earlier) - we'll post a notice in plugin changes issue and call it a day. If someone really needs to prevent using such additions, the plugin update is trivial and arguably the plugin code was incorrect if it didn't handle |
Gotcha. I am working on creating the test case and submitting the PR but I ran into a small issue. After making the changes, I created a test case with this as the plugin from mypy.types import LiteralType, AnyType, TypeOfAny
from mypy.plugin import Plugin
def type_add(ctx, *args):
lhs = ctx.type
rhs = ctx.arg_types[0][0]
ret = lhs.value + rhs.value
ctx.api.fail("test", ctx.context)
return AnyType(TypeOfAny.from_error)
def type_radd(ctx, *args):
lhs = ctx.type
rhs = ctx.arg_types[0][0]
ret = lhs.value + rhs.value
return LiteralType(ret, fallback=ctx.api.named_generic_type('builtins.int', []))
class TestPlugin(Plugin):
def get_method_hook(self, fullname):
if fullname == 'builtins.int.__add__':
return type_add
if fullname == 'builtins.int.__radd__':
return type_radd
return None
def plugin(version: str):
return TestPlugin and this test: from typing import Literal
op1: Literal[3] = 3
op2: Literal[4] = 4
c = op1 + op2
reveal_type(c) # N: Revealed type is "Literal[7]" check-custom-plugin.test:
For whatever reason, whenever I use
Meaning it is not flipping the operands.
I am printing these hooks from mypy code itself (temporarily, to figure out why the test is failing), not the plugin, so I am not sure why there is a difference in the output. for name, method, obj, arg in variants:
with self.msg.filter_errors(save_filtered_errors=True) as local_errors:
print(name)
result = self.check_method_call(name, obj, method, [arg], [ARG_POS], context) This code is also on my Github. I have never used pytest before, so it might be a pytest thing that I am messing up. But I am not sure why that would be the case or how to fix it. Thank you for your help! |
It isn't a pytest thing, but our complicated test setup:) Every test can optionally end with
If you add such a line, it will treat the provided file as a replacement for So, in that stub
to your test (all those fixtures live in |
Thank you for your help, hopefully the PR goes through soon. |
I recommend leaving this issue open, and writing "Fixes <issue link>" in the initial PR comment (you can edit it in). This way, this issue will automatically close when that PR is merged! And if the PR is closed, this issue is still open so others can see it's still a problem. |
Gotcha, my bad. |
…thod>__ hook (#18995) Fixes #18945 (This was a feature request that turned out to be a bug) Essentially, I was trying to create a custom plugin and implement __add__. In the process I discovered that __radd__ would implicitly be called if __add__ has an error but it would be called under the same __add__ hook (instead of radd) which is bad for non-commutative operations. This has been fixed. I also added a test for it.
Feature
When type checking an overloaded magic method, mypy will implicitly reverse the operands and check it with the reversed operands. A flag to prevent this would be very helpful
Pitch
Some operations are not commutative and checking the reverse may end up with a different result causing confusion and errors. I am making a plugin and when checking
a * b
, whether or not it fails, it will also checkb * a
. This is incredibly annoying as if the former fails, it will check the latter. If the latter is successful, it will overwrite the failure. Being able to prevent this, either via a flag or some other way would be super nice, especially when writing plugins.The text was updated successfully, but these errors were encountered: