facebook / facebook/pyrefly

pyrefly ignores `__new__` or `__call__` overloads

Open
#3,878 7 comments 0 reactions 0 assignees View on GitHub
needs-discussion typechecking
Dominant language
Rust
Stars
7k
Forks
516
PR merge metrics
No merged PRs in 30d

Description

### Describe the Bug

I have a file declaring a `class` with an `@overload`ed `__new__` method, e.g.
```py
"""Pyrefly reproducer."""

from __future__ import annotations

import abc
from typing import overload

class Thing(abc.ABC):

@overload
def __new__(cls, first: int, **kwargs) -> IntThing:
...

@overload
def __new__(cls, first: float, **kwargs) -> FloatThing:
...

def __new__(cls, first: int | float, **kwargs) -> Thing:
if isinstance(first, int):
return super().__new__(IntThing)
elif isinstance(first, float):
return super().__new__(FloatThing)
else:
raise ValueError("I'm sorry, Dave. I'm afraid I can't do that.")

def __init__(self, *, extra_stuff):
print(f'Got extra_stuff={extra_stuff}.')

@overload
@classmethod
def __call__(cls, first: int, **kwargs) -> IntThing:
...

@overload
@classmethod
def __call__(cls, first: float, **kwargs) -> FloatThing:
...

@classmethod
def __call__(
cls, first: int | float, *args, **kwargs
) -> IntThing | FloatThing:
if isinstance(first, int):
return IntThing(first, *args, **kwargs)
elif isinstance(first, float):
return FloatThing(first, *args, **kwargs)
else:
raise ValueError("I'm sorry, Dave. I'm afraid I can't do that.")

class IntThing(Thing):

def __init__(self, value: int):
super().__init__(extra_stuff='integers')

class FloatThing(Thing):

def __init__(self, value: float):
super().__init__(extra_stuff='floats')
```
i.e. `Thing` is an abstract base class that works as a factory for its derived classes. I really want this so that users can do stuff like
```py
"""Pyrefly reproducer."""

from . import things

def test():
thing_one = things.Thing(1)
thing_two = things.Thing(1.0)

assert isinstance(thing_one, things.Thing)
assert isinstance(thing_two, things.Thing)
```
i.e. `Thing` is both the correct type _and_ the factory, and users don't even have to know what's going on under the hood.

The problem, though, is that the above fails with
```
ERROR Expected argument `extra_stuff` to be passed by name in function `things.Thing.__init__` [unexpected-positional-argument]
--> client.py:6:28
|
6 | thing_one = things.Thing(1)
| ^
|
ERROR Expected argument `extra_stuff` to be passed by name in function `things.Thing.__init__` [unexpected-positional-argument]
--> client.py:7:28
|
7 | thing_two = things.Thing(1.0)
| ^^^
|
```
i.e. `pyrefly` does not seem to understand that creating a new `Thing(...)` has been overloaded, and checks the provided arguments against `Thing`'s `__init__` method, and not `FloatThing`'s (as the above `@overload`s) would imply.

I also tried overriding `Thing.__call__(cls, first, **kwargs)`, but the type checks fail with the same error message.

I would have expected that `pyrefly` would check whether a class' `__call__` or `__new__` functions for overloads before checking the constructor args, but maybe I'm just holding this wrong?

### Sandbox Link

https://pyrefly.org/sandbox/?project=N4IgZglgNgpgziAXKOBDAdgEwEYHsAeAdAA4CeSIAOlTQAqkBOMYUpABE8Q7pgK4DGMBoWqiq6SuggBbYrgYAXNgoAWEdAHM4kyZmbL4CgBQBKRJLbK1mgPq50MNgF4r6rYQAq1jUYCMJi1dbBQB3XGcg9y83P0IABgCJdEtUODghJQg4dTgFDEEjVTc7BwAaSLhPb0SUtIy2LJy89AKi4LDytqjqyRBSkDImFlJCBVxpKAoAYjZ6IdY2NCw8fDZ%2Be0gNXgZUBQh7EXQZgGUYRxUFBWI4RAB6W8HmVkJ5DVuYdFvMXH44W-X0Jttrt9p82GB5GxUAA3VDQVDYWBrDYQLY7Pb2Ni4YgY9CVSRkVT2AC00KE2UxLmoAGZCL4AEyidAAbSE3AYcAAupJeFJZPIFDBMMTMBAmPw9mSIgByVHoeQwaWSeUKYlMACOvDFQuJAGsYKRiah%2BII0jKQqgGOgleg%2BiAupUyBQxNQ5k92JxuHxBMIXeJJGBuNI2DYbGBeAptjBQw1%2BYooegVSD7NokjI5PGEfwA0HlKRiG5YxmlLgyQwoLhUJgdEl%2BFBUnA2NFNEYs4QAIIAIQAwmYa5YAAKloQVquBPRgEM2BwhUNGOtwcqQDkKRANdAKcoAKi3uotDC0JjYxIAfGwAJIb5saczJSxsQiP-tsIdl0fVu8TqczucLpdi3I1xYStNzYHc90tQ9jzPAAxUcFGvW97wfJ8kksL9Qx-Gx5ygRdwQA1d1yUAAfcF4O3Xd9yg08m28JD7wgSdGjxZoCmXXJynUBQ%2BzvZCmEjK1Fl4YghFMQhMJgWdsMvBCel4tgYCgRiGmyFj8hgIx2NA4Ddh45DLH47ZkjgYTRJMcTp0kuc4JA68anvRT0novi4XSNgADVUCgXgYAAUQYdkjGoc9pWDOB5AYUhygAERhGBCAvUKoUDOFMAvNYMGlJRvisXYRBARJx30UN1AgBQ53SKAwG3coYHwBQdhsXJeDAMA9PvLguM06UAHFcCUOqGtQJrI1apxgEGxrmtagBfQhpUKu9XxHSsP0HOsG2kGAiTWtgMJsfgvKgX9cP-Fc1y4iiIIPOAjxomTEMCSxH0OQJlvLVa3o2tItp2orJ1DQ6oGO7C-3w86yJAq6qNu6C2Bs3ZHvkl7nwHb64F%2BlQeH%2BqcgZBoL5MsMGtIujc2FInTQK3SC8PAmHAjus8Hu8cn4fgpHkOU5jcnUzSCM4jd2r47ajIvK9vD5ldtxp6GafsyxFK51SeZaDStKXeChfvQzBIR2SYnVsCZbAyi5aehTcJgZztdcxxPO8vyAvkIKQBCsKIqithYrJBK3eSnYIDS88MutbLwlUPLqEWyR0bF-WWzs28cZKqRyuwyrqrYWEHdJ7jrZMkSGDElOyrnSbhumsAnFlDcYA0ckFprGP6zNPXryMRPn320q06MDPymznygM1-PTKL8yS978uRpaqvpUpuBG9tGb%2BmNSUYBg6AYAoB0SHIGagA

### (Only applicable for extension issues) IDE Information

_No response_

Contributor guide

Open the contributing guide

Research direction

Reproduce the diagnostic with the linked Pyrefly sandbox using the Thing.__new__ and __call__ overload examples; start by tracing how class construction checks __init__ versus overloaded entry points. Done means Thing(1) and Thing(1.0) are checked against the appropriate overloads without the unexpected-positional-argument errors, with coverage for both __new__ and __call__ behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, rust
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.