-
Notifications
You must be signed in to change notification settings - Fork 451
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
floatshould 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
floatshould 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 offloatitself 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
floator 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.floorandmath.ceilby making them do an unchecked cast of their argument fromRealtoUnion[float, subclass of Real which defines __ceil__ and __floor__]. -
Modify typeshed to pretend
floatdefines__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.ceilandmath.floorstill work" privilege fromfloattofloatsubclasses.
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?