Description
Consider the following example:
from typing import NewType
T = NewType('T', float)
T(1.0)
Pyre expands the NewType definition into:
class T(float):
def __init__(self, input: float) -> None: pass
which fails with:
example.py:3:0 Invalid class instantiation [45]:
Cannot instantiate abstract class `T` with abstract methods `__ceil__`, `__floor__`.
This is a very awkward case. Python's float
is subclass of ABC numbers.Real
(which is hardwired into Pyre The relevant declarations in typeshed (irrelevant methods elided, ...
s in original) are:
class Real(Complex, SupportsFloat):
@abstractmethod
def __floor__(self) -> int: ...
@abstractmethod
def __ceil__(self) -> int: ...
class float:
if sys.version_info >= (3, 9):
def __ceil__(self) -> int: ...
def __floor__(self) -> int: ...
(from numbers.pyi and builtins.pyi)
In fact, float
on CPython 3.7 and 3.8 does appear to be missing __ceil__
and __float__
methods. This doesn't break users because the exposed API for ceil/floor is through the functions math.ceil
and math.floor
which handle floats internally and only delegate to __ceil__
and __floor__
when called on a non-float
.
This doesn't cause Pyre to explode normally presumably because AbstractClassInstantiation
failures are internally suppressed for int
/float
/bool
. But, that suppression trick fails for subclasses of builtins.
This is a kinda gross situation, and there are no obvious good fixes. Some ideas:
-
Argue that
float
should properly be considered an abstract type (pre-3.9), since allowing instances would mean thatsomeFloat.__ceil__()
would pass the typechecker but fail at runtime. Take a hard-line approach and raise type errors. This is obviously insane. -
Argue that
float
should properly be considered an abstract type (pre-3.9). Admit that it's impractical to prevent people from instantiating floats, and say that the minimum breakage would be to allow instantiation offloat
itself but nothing else. (This is the status quo.) -
Attempt to have Pyre capture the actual Python behavior, which is something like "requiring instantiation of subclasses of Real (except
float
or its subclasses) to define__ceil__
and__floor__
, but treat attempts to use__ceil__
or__floor__
as type errors. This seems like a lot of implementation hacks for relatively little benefit. -
Remove
__ceil__
and__floor__
from Real. This basically just moves the problem tomath.floor
andmath.ceil
by making them do an unchecked cast of their argument fromReal
toUnion[float, subclass of Real which defines __ceil__ and __floor__]
. -
Modify typeshed to pretend
float
defines__ceil__
and__floor__
for all Python versions. This seems minimally objectionable. Pyre already thinksfloat.__ceil__
andfloat.__floor__
are ok becausefloat <: Real
. All it changes is extending the "I can be an instance of Real without actually implementing__ceil__
/__floor__
as long asmath.ceil
andmath.floor
still work" privilege fromfloat
tofloat
subclasses.
Of these, I'd lean towards option 5, and I'll put a diff up for that. There might be some better idea I didn't think of.
Thoughts?