Repository navigation
pow(complex(…), <value>, <value>) and pow(float(…), <value>, <value>) should produce type errors #6303
Description
Activity
I don't think the fix can be as simple as simply removing the third argument of
float.__pow__/complex.__pow__, given that the following all succeed at runtime without error:>>> pow(1.0, 2, None) 1.0 >>> 1.0.__pow__(2, None) 1.0 >>> pow(complex(1), 2, None) (1+0j) >>> complex(1).__pow__(3, None) (1+0j)
Perhaps if we changed
builtins._M(line 1283) fromfrom typing import TypeVar _M = TypeVar("_M", contravariant=True)
to
from typing import TypeVar, SupportsIndex _M = TypeVar("_M", contravariant=True, bound=SuppprtsIndex)
?
And, to be fair, this works just fine at runtime:
>>> pow(complex(1), 2, None) (1+0j)
In the
boundproposal, is the intention to useSupportsIndexas a proxy fornumbers.Integral-like things? (Kind of like howmath.truncacceptsSupportsFloat?)- addedstubs: false negativeType checkers do not report an error, but shouldType checkers do not report an error, but should
on Nov 15, 2021 In the
boundproposal, is the intention to useSupportsIndexas a proxy fornumbers.Integral-like things? (Kind of like howmath.truncacceptsSupportsFloat?)Yeah, sort of. I think Mypy currently thinks
pow(2.5, 3.9, 6)is okay because of the final overload ofpowin line 1309, where we've told it thatpowcan be used with three arguments as long as the type passed in for argument 1 is annotated as having a__pow__method that accepts three arguments.__float__and__complex__both are, and that's correct, because of course theNoneargument can be passed in explicitly at runtime! But the_MTypeVar used to parameterize the_SupportsPow3protocol isn't restrictive enough.Gotta go but some WIP experiments here https://mypy-play.net/?mypy=latest&python=3.10&flags=show-error-codes%2Cstrict&gist=b079cabb8620757ef16301a2dc12a352
Reacted by Matt BogosianHmm, it's not the best error message, but this does seem to work: https://mypy-play.net/?mypy=latest&python=3.10&flags=show-error-codes%2Cstrict&gist=4e463e07a3a48610e140a4c98c18b0ca
It involves changing the 5th overload from
def pow(base: float, exp: float, mod: None = ...) -> Any: ...
to
def pow(base: complex, exp: complex, mod: None = ...) -> Any: ...
as well as the change to the
_MTypeVar.But, I haven't yet tested it with classes outside builtins.
Reacted by Matt BogosianThat's some expert level deep typing voodoo.
I just thought of another wrinkle. Mypy treats
floats as equivalent toints for many (all?) purposes. That may make guarding against that particular case problematic.I also just noticed that
Decimalis weird (but nobody cares aboutDecimal, so oh well, I guess):>>> pow(Decimal("1.0"), Decimal("1.0"), Decimal("1.0")) # works Decimal('0')
Reacted by Alex WaygoodWeird false-positive error in this implementation if you try to do
pow(Fraction, complex)(though honestly, why would you??)This is a better solution, I think -- introduce a third protocol to deal with classes that accept "three-argument
powbut only if the third argument isNone", rather than mucking around with binding the_MTypeVar. It even means that we can deal with theDecimalweirdness!from typing import TypeVar, Protocol, Any, Literal, NoReturn, overload _E = TypeVar("_E", contravariant=True) _M = TypeVar("_M", contravariant=True) _T_co = TypeVar("_T_co", covariant=True) class _SupportsPow2(Protocol[_E, _T_co]): def __pow__(self, other: _E) -> _T_co: ... class _SupportsPow3NoneOnly(Protocol[_E, _T_co]): def __pow__(self, other: _E, modulo: None = ...) -> _T_co: ... class _SupportsPow3(Protocol[_E, _M, _T_co]): def __pow__(self, other: _E, modulo: _M = ...) -> _T_co: ... @overload def pow(base: int, exp: int, mod: Literal[0]) -> NoReturn: ... # int base & positive-int exp -> int; int base & negative-int exp -> float # return type must be Any as `int | float` causes too many false-positive errors @overload def pow(base: int, exp: int, mod: None = ...) -> Any: ... @overload def pow(base: int, exp: int, mod: int) -> int: ... @overload def pow(base: float, exp: int, mod: None = ...) -> float: ... # float base & float exp could return float or complex # return type must be Any as `float | complex` causes too many false-positive errors @overload def pow(base: complex, exp: complex, mod: None = ...) -> Any: ... @overload def pow(base: _SupportsPow2[_E, _T_co], exp: _E, mod: None = ...) -> _T_co: ... @overload def pow(base: _SupportsPow3NoneOnly[_E, _T_co], exp: _E, mod: None = ...) -> _T_co: ... @overload def pow(base: _SupportsPow3[_E, _M, _T_co], exp: _E, mod: _M = ...) -> _T_co: ...
There's still a false positive with
Fraction ** complexthough, and I can't work out why.Reacted by Matt BogosianReacted by Matt BogosianI definitely need to up my
TypeVargame. The thing that set me on this path was trying to type a helpfulpowwrapper that would work withnumerary(which has much less sophisticatedSupportsComplexPowandSupportsIntegralPowprotocols more akin tonumbers.Complexandnumbers.Integral).I don't think this actually solves your original problem, but, er, here's a PR fixing some other problems with
pow! #6325Reacted by Matt BogosianReacted by Matt Bogosian@posita, I think this should maybe be refiled as a mypy issue now, since we now get error messages in the right places, it's just not the right kind of error messages. That feels like more of a mypy issue rather than a typeshed issue -- WDYT? 🙂
@posita, I think this should maybe be refiled as a mypy issue now, since we now get error messages in the right places, it's just not the right kind of error messages. That feels like more of a mypy issue rather than a typeshed issue -- WDYT? 🙂
Closing as per my comments here
Reacted by Matt BogosianReacted by Matt Bogosian
complex's versions of__pow__and__rpow__invite the third modulo argument. That means that the following validates, when it shouldn't:This applies to
floats as well.These are guaranteed to fail at runtime, even if the first arguments are losslessly convertible to
ints:I can't find an issue tracking this, and I don't think this is fixed by #6287.
As an aside, this "works" (in the sense it properly generates some error, but not what I would expect):
That second error might just be some variation on python/mypy#3186, however?