Skip to content

Commit

Permalink
work around awkward situation with float subclasses
Browse files Browse the repository at this point in the history
Summary:

On python before 3.9, `float` is a subclass of the ABC numbers.Real which
defines abstract methods `__floor__` and `__ceil__`, but doesn't provide
an actual implementation of those methods. This is arguably a type error
in Python, because the following code is well-typed:

    def foo(x: numbers.Real): return x.__floor()
    def bar(x: float): return foo(x)
    bar(some float)

but fail at runtime. This doesn't create a user-visible problem because
`__floor__` and `__ceil__` aren't meant to be called directly; instead,
users are supposed to call `math.floor(x)` and `math.ceil(x)` which are
fine without the dunder methods as long as the object is a primitive
number (or a subclass of one, or can be converted to one in various
ways.)

This creates the unfortunate situation that Pyre won't let people
instantiate subclasses of `float` without definitions of `__floor__` and
`__ceil__`, even though no typical code actually requires those methods.
(Pyre currently has a special case to allow the instantiation of `float`
itself.)

The ideal behavior would be if Pyre treated `numbers.Real` as "in order
to be a subtype of `numbers.Real`, you need to implement the floor/ceil
dunder methods, or be a subtype of int/float/etc, or implement
`__float__` or `__index__`, etc." and "numbers.Real are not guaranteed
to define `__floor__` and `__ceil__`. This would be insane to implement.

Pretending that `float` defines `__floor__` and `__ceil__` is a
minimally objectionable workaround. It means that float will continue to
work, subclasses of float will start to work, and the only well-typed
code which can error at runtime is `val.__floor__()` when
`type(val) == T <: float`... But that problem already exists when
`T == float` and so I don't find extending it to subclasses of float to
be particularly concerning.

Resolves facebook#527 (github)

Test plan:

These two examples fail to typecheck before:

    # example 1
    import typing
    T = typing.NewType('T', float)
    T(4.0)

    # example 2
    class C(float):
        def __init__(self, v: float) -> None: pass
    C(4.0)

with errors:

    example.py:3:0 Invalid class instantiation [45]:
        Cannot instantiate abstract class `T` with abstract methods `__ceil__`, `__floor__`.

    example.py:9:14 Invalid class instantiation [45]:
        Cannot instantiate abstract class `C` with abstract methods `__ceil__`, `__floor__`.

but both pass after.
  • Loading branch information
jesboat committed Nov 5, 2021
1 parent d3cbb71 commit 84d5614
Showing 1 changed file with 9 additions and 0 deletions.
9 changes: 9 additions & 0 deletions scripts/typeshed-patches/stdlib/builtins.patch
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@
def __truediv__(self, x: int) -> float: ...
def __mod__(self, x: int) -> int: ...
def __divmod__(self, x: int) -> Tuple[int, int]: ...
@@ -279,7 +282,7 @@
def __rpow__(self, x: float, mod: None = ...) -> float: ...
def __getnewargs__(self) -> Tuple[float]: ...
def __trunc__(self) -> int: ...
- if sys.version_info >= (3, 9):
+ if sys.version_info >= (0, 0):
def __ceil__(self) -> int: ...
def __floor__(self) -> int: ...
@overload
@@ -367,7 +370,12 @@
def isspace(self) -> bool: ...
def istitle(self) -> bool: ...
Expand Down

0 comments on commit 84d5614

Please sign in to comment.