Skip to content

Conversation

@dhruvmanila
Copy link
Member

@dhruvmanila dhruvmanila commented May 30, 2025

Summary

Part of astral-sh/ty#104, closes: astral-sh/ty#468

This PR implements the argument type expansion which is step 3 of the overload call evaluation algorithm.

Specifically, this step needs to be taken if type checking resolves to no matching overload and there are argument types that can be expanded.

Test Plan

Add new test cases.

Ecosystem analysis

This PR removes 174 no-matching-overload false positives -- I looked at a lot of them and they all are false positives.

One thing that I'm not able to understand is that in https://github.com/sphinx-doc/sphinx/blob/2b7e3adf27c158305acca9b5e4d0d93d3e4c6f09/sphinx/ext/autodoc/preserve_defaults.py#L179 the inferred type of value is str | None by ty and Pyright, which is correct, but it's only ty that raises invalid-argument-type error while Pyright doesn't. The constructor method of DefaultValue has declared type of str which is invalid.

There are few cases of false positives resulting due to the fact that ty doesn't implement narrowing on attribute expressions.

@dhruvmanila dhruvmanila added the ty Multi-file analysis & type inference label May 30, 2025
@github-actions
Copy link
Contributor

github-actions bot commented May 30, 2025

mypy_primer results

Changes were detected when running on open source projects
pytest-robotframework (https://github.com/detachhead/pytest-robotframework)
- error[no-matching-overload] pytest_robotframework/__init__.py:569:9: No overload of function `keyword` matches arguments
- Found 228 diagnostics
+ Found 227 diagnostics

anyio (https://github.com/agronholm/anyio)
- error[no-matching-overload] src/anyio/_core/_sockets.py:279:12: No overload of function `fspath` matches arguments
- error[no-matching-overload] src/anyio/_core/_sockets.py:543:19: No overload of function `fspath` matches arguments
- Found 111 diagnostics
+ Found 109 diagnostics

ignite (https://github.com/pytorch/ignite)
- error[no-matching-overload] ignite/utils.py:345:14: No overload of bound method `__init__` matches arguments
- Found 2254 diagnostics
+ Found 2253 diagnostics

rich (https://github.com/Textualize/rich)
- error[no-matching-overload] rich/progress.py:496:14: No overload of bound method `open` matches arguments
- warning[unused-ignore-comment] rich/progress.py:506:44: Unused blanket `type: ignore` directive
- Found 394 diagnostics
+ Found 392 diagnostics

trio (https://github.com/python-trio/trio)
- error[no-matching-overload] src/trio/_highlevel_open_unix_stream.py:63:28: No overload of function `fspath` matches arguments
- Found 1094 diagnostics
+ Found 1093 diagnostics

check-jsonschema (https://github.com/python-jsonschema/check-jsonschema)
- error[no-matching-overload] src/check_jsonschema/cli/param_types.py:135:26: No overload of function `fspath` matches arguments
- Found 66 diagnostics
+ Found 65 diagnostics

PyWinCtl (https://github.com/Kalmat/PyWinCtl)
- error[no-matching-overload] src/pywinctl/_pywinctl_linux.py:161:21: No overload of function `compile` matches arguments
- error[no-matching-overload] src/pywinctl/_pywinctl_linux.py:215:20: No overload of function `compile` matches arguments
- Found 38 diagnostics
+ Found 36 diagnostics

nox (https://github.com/wntrblm/nox)
- error[no-matching-overload] nox/_options.py:160:12: No overload of function `fspath` matches arguments
- error[no-matching-overload] nox/command.py:84:33: No overload of function `fspath` matches arguments
- error[no-matching-overload] nox/command.py:160:22: No overload of function `fspath` matches arguments
- error[no-matching-overload] nox/command.py:161:17: No overload of function `fspath` matches arguments
- Found 37 diagnostics
+ Found 33 diagnostics

schemathesis (https://github.com/schemathesis/schemathesis)
- error[no-matching-overload] src/schemathesis/core/rate_limit.py:15:13: No overload of function `urlparse` matches arguments
- error[no-matching-overload] src/schemathesis/specs/openapi/checks.py:564:14: No overload of function `urlparse` matches arguments
- Found 414 diagnostics
+ Found 412 diagnostics

tornado (https://github.com/tornadoweb/tornado)
- error[no-matching-overload] tornado/auth.py:1221:12: No overload of function `quote` matches arguments
- error[no-matching-overload] tornado/concurrent.py:177:9: No overload of function `future_add_done_callback` matches arguments
- error[no-matching-overload] tornado/curl_httpclient.py:386:43: No overload of function `to_unicode` matches arguments
- error[no-matching-overload] tornado/curl_httpclient.py:485:38: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/escape.py:59:24: No overload of function `to_unicode` matches arguments
- error[no-matching-overload] tornado/escape.py:77:26: No overload of function `to_unicode` matches arguments
- error[no-matching-overload] tornado/escape.py:128:12: No overload of function `quote_plus` matches arguments
- error[no-matching-overload] tornado/escape.py:128:12: No overload of function `quote` matches arguments
- error[no-matching-overload] tornado/escape.py:166:21: No overload of function `to_unicode` matches arguments
- error[no-matching-overload] tornado/escape.py:170:24: No overload of function `to_unicode` matches arguments
- error[no-matching-overload] tornado/httpclient.py:571:22: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/httputil.py:1162:12: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/httputil.py:1162:36: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/routing.py:535:33: No overload of function `compile` matches arguments
- error[no-matching-overload] tornado/routing.py:570:26: No overload of function `compile` matches arguments
- error[no-matching-overload] tornado/template.py:317:40: No overload of function `to_unicode` matches arguments
- error[no-matching-overload] tornado/web.py:685:17: No overload of function `to_unicode` matches arguments
- error[invalid-argument-type] tornado/web.py:2293:44: Argument to bound method `__init__` is incorrect: Expected `Pattern[Unknown]`, found `Unknown | str | Pattern[Unknown]`
+ error[invalid-argument-type] tornado/web.py:2293:44: Argument to bound method `__init__` is incorrect: Expected `Pattern[Unknown]`, found `Unknown | Pattern[str] | str | Pattern[Unknown]`
- error[no-matching-overload] tornado/web.py:3561:30: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/web.py:3583:43: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/web.py:3654:13: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/web.py:3764:13: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/web.py:3777:21: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/web.py:3779:21: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/web.py:3784:21: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/websocket.py:470:16: No overload of function `utf8` matches arguments
- error[no-matching-overload] tornado/websocket.py:919:21: No overload of function `utf8` matches arguments
- Found 353 diagnostics
+ Found 327 diagnostics

cki-lib (https://gitlab.com/cki-project/cki-lib)
- error[no-matching-overload] cki_lib/yaml.py:91:22: No overload of function `dirname` matches arguments
- Found 170 diagnostics
+ Found 169 diagnostics

vision (https://github.com/pytorch/vision)
- error[no-matching-overload] torchvision/datasets/eurosat.py:37:21: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/eurosat.py:53:21: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/folder.py:63:17: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/imagenet.py:53:28: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/imagenet.py:53:28: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/utils.py:103:12: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/utils.py:148:12: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/utils.py:165:12: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/utils.py:193:12: No overload of function `expanduser` matches arguments
- error[no-matching-overload] torchvision/datasets/utils.py:312:32: No overload of function `fspath` matches arguments
- error[no-matching-overload] torchvision/datasets/utils.py:353:19: No overload of function `dirname` matches arguments
- error[no-matching-overload] torchvision/datasets/utils.py:359:35: No overload of function `basename` matches arguments
- error[no-matching-overload] torchvision/datasets/utils.py:382:21: No overload of function `expanduser` matches arguments
- Found 1546 diagnostics
+ Found 1533 diagnostics

urllib3 (https://github.com/urllib3/urllib3)
- error[no-matching-overload] src/urllib3/connectionpool.py:1150:12: No overload of function `_normalize_host` matches arguments
- error[no-matching-overload] src/urllib3/util/url.py:438:16: No overload of function `_normalize_host` matches arguments
- error[no-matching-overload] test/test_ssltransport.py:56:24: No overload of function `sample_request` matches arguments
- Found 509 diagnostics
+ Found 506 diagnostics

scrapy (https://github.com/scrapy/scrapy)
- error[no-matching-overload] scrapy/mail.py:143:13: No overload of bound method `set_payload` matches arguments
- error[no-matching-overload] scrapy/mail.py:143:13: No overload of bound method `set_payload` matches arguments
- Found 1334 diagnostics
+ Found 1332 diagnostics

mitmproxy (https://github.com/mitmproxy/mitmproxy)
- error[no-matching-overload] mitmproxy/io/har.py:111:24: No overload of function `encode` matches arguments
- error[no-matching-overload] mitmproxy/net/http/url.py:54:40: No overload of function `urlparse` matches arguments
- warning[unused-ignore-comment] mitmproxy/net/http/url.py:60:71: Unused blanket `type: ignore` directive
+ error[invalid-assignment] mitmproxy/net/http/url.py:54:5: Object of type `ParseResult | ParseResultBytes` is not assignable to `ParseResult`
+ warning[possibly-unbound-attribute] mitmproxy/net/http/url.py:58:16: Attribute `encode` on type `str | None` is possibly unbound
- error[no-matching-overload] mitmproxy/tools/web/app.py:102:21: No overload of function `always_str` matches arguments
- error[no-matching-overload] mitmproxy/tools/web/app.py:119:21: No overload of function `always_str` matches arguments
- error[no-matching-overload] test/mitmproxy/net/test_encoding.py:60:24: No overload of function `decode` matches arguments
- Found 2111 diagnostics
+ Found 2107 diagnostics

mypy (https://github.com/python/mypy)
- error[no-matching-overload] mypy/checker.py:762:22: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checker.py:2240:25: No overload of function `get_proper_type` matches arguments
+ error[invalid-argument-type] mypy/checker.py:2309:28: Argument to function `is_equivalent` is incorrect: Expected `Type`, found `None | (ProperType & ~AnyType)`
+ error[invalid-argument-type] mypy/checker.py:2317:33: Argument to function `is_subtype` is incorrect: Expected `Type`, found `None | (ProperType & ~AnyType)`
- error[no-matching-overload] mypy/checker.py:3749:15: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checker.py:4532:28: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checker.py:5310:42: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checker.py:6339:48: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checker.py:7618:25: No overload of function `conditional_types` matches arguments
- error[no-matching-overload] mypy/checker.py:7992:23: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checker.py:8742:9: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checker.py:8752:11: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checkexpr.py:438:24: No overload of function `get_proper_type` matches arguments
+ error[invalid-return-type] mypy/checkexpr.py:444:28: Return type does not match returned value: expected `Type`, found `Unknown | LiteralType | None`
- error[no-matching-overload] mypy/checkexpr.py:1032:23: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checkexpr.py:1469:23: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checkexpr.py:2134:29: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checkexpr.py:5346:19: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checkexpr.py:6672:15: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/checkmember.py:1494:21: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/join.py:894:16: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/meet.py:529:19: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/messages.py:839:27: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/messages.py:1069:23: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/plugins/common.py:180:28: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/plugins/dataclasses.py:795:27: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/plugins/dataclasses.py:919:13: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/plugins/functools.py:125:35: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/plugins/singledispatch.py:102:17: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/plugins/singledispatch.py:131:22: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/solve.py:306:16: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/stats.py:358:13: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/stubtest.py:1149:27: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/subtypes.py:1540:15: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/typeops.py:217:27: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/typeops.py:287:22: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/typeops.py:396:25: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/typeops.py:458:32: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/typeops.py:474:21: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypy/typeops.py:993:9: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypyc/irbuild/classdef.py:343:20: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypyc/irbuild/classdef.py:641:20: No overload of function `get_proper_type` matches arguments
- error[no-matching-overload] mypyc/irbuild/expression.py:224:11: No overload of function `get_proper_type` matches arguments
- Found 3262 diagnostics
+ Found 3225 diagnostics

pytest (https://github.com/pytest-dev/pytest)
- error[no-matching-overload] src/_pytest/_code/code.py:392:43: No overload of function `fspath` matches arguments
- error[no-matching-overload] src/_pytest/config/argparsing.py:113:20: No overload of function `fspath` matches arguments
- error[no-matching-overload] src/_pytest/config/argparsing.py:170:20: No overload of function `fspath` matches arguments
- error[no-matching-overload] src/_pytest/pathlib.py:1004:17: No overload of function `abspath` matches arguments
- error[no-matching-overload] src/_pytest/pytester.py:990:18: No overload of function `abspath` matches arguments
- error[no-matching-overload] src/_pytest/pytester.py:1405:25: No overload of function `fspath` matches arguments
- error[no-matching-overload] src/_pytest/reports.py:371:32: No overload of function `fspath` matches arguments
- error[no-matching-overload] testing/python/collect.py:1232:16: No overload of function `fspath` matches arguments
- error[no-matching-overload] testing/python/collect.py:1247:16: No overload of function `fspath` matches arguments
- error[no-matching-overload] testing/test_recwarn.py:568:9: No overload of function `warn` matches arguments
- Found 787 diagnostics
+ Found 777 diagnostics

werkzeug (https://github.com/pallets/werkzeug)
- error[no-matching-overload] src/werkzeug/_reloader.py:285:20: No overload of function `abspath` matches arguments
- error[no-matching-overload] src/werkzeug/datastructures/accept.py:86:16: No overload of function `__getitem__` matches arguments
- error[no-matching-overload] src/werkzeug/datastructures/file_storage.py:194:28: No overload of function `fspath` matches arguments
- error[no-matching-overload] src/werkzeug/utils.py:426:20: No overload of function `abspath` matches arguments
- error[no-matching-overload] src/werkzeug/utils.py:564:26: No overload of function `fspath` matches arguments
- error[no-matching-overload] src/werkzeug/utils.py:564:48: No overload of function `fspath` matches arguments
- Found 478 diagnostics
+ Found 472 diagnostics

altair (https://github.com/vega/altair)
- error[no-matching-overload] tests/__init__.py:159:10: No overload of function `compile` matches arguments
- Found 1297 diagnostics
+ Found 1296 diagnostics

discord.py (https://github.com/Rapptz/discord.py)
- error[no-matching-overload] discord/ext/commands/help.py:668:13: No overload of bound method `sort` matches arguments
+ warning[unused-ignore-comment] discord/ext/commands/help.py:648:75: Unused blanket `type: ignore` directive
+ warning[unused-ignore-comment] discord/ext/commands/help.py:652:75: Unused blanket `type: ignore` directive
- Found 619 diagnostics
+ Found 620 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- error[no-matching-overload] static_frame/core/util.py:1363:21: No overload of bound method `__init__` matches arguments
- Found 1943 diagnostics
+ Found 1942 diagnostics

meson (https://github.com/mesonbuild/meson)
- error[no-matching-overload] mesonbuild/mlog.py:571:54: No overload of function `basename` matches arguments
- Found 1334 diagnostics
+ Found 1333 diagnostics

bokeh (https://github.com/bokeh/bokeh)
- error[no-matching-overload] src/bokeh/command/subcommands/serve.py:1004:11: No overload of function `format_docstring` matches arguments
- error[no-matching-overload] src/bokeh/embed/server.py:408:27: No overload of function `format_docstring` matches arguments
- error[no-matching-overload] src/bokeh/embed/server.py:409:26: No overload of function `format_docstring` matches arguments
- error[no-matching-overload] src/bokeh/server/tornado.py:810:24: No overload of function `format_docstring` matches arguments
- error[no-matching-overload] src/bokeh/util/serialization.py:88:11: No overload of function `format_docstring` matches arguments
- error[no-matching-overload] src/bokeh/util/serialization.py:319:35: No overload of function `format_docstring` matches arguments
- Found 946 diagnostics
+ Found 940 diagnostics

sphinx (https://github.com/sphinx-doc/sphinx)
- error[no-matching-overload] sphinx/builders/html/__init__.py:1139:26: No overload of function `fspath` matches arguments
- error[no-matching-overload] sphinx/builders/html/__init__.py:1167:31: No overload of function `fspath` matches arguments
- error[no-matching-overload] sphinx/builders/html/_assets.py:166:16: No overload of function `fspath` matches arguments
- error[call-non-callable] sphinx/config.py:205:16: Method `__getitem__` of type `Overload[(key: SupportsIndex, /) -> Unknown, (key: slice[Any, Any, Any], /) -> tuple[Unknown, ...]]` is not callable on object of type `tuple[Unknown, Unknown, Unknown]`
- error[no-matching-overload] sphinx/ext/autodoc/preserve_defaults.py:178:33: No overload of function `unparse` matches arguments
+ error[invalid-argument-type] sphinx/ext/autodoc/preserve_defaults.py:179:72: Argument to bound method `__init__` is incorrect: Expected `str`, found `str | None`
- error[no-matching-overload] sphinx/ext/autodoc/type_comment.py:96:52: No overload of function `unparse` matches arguments
- error[no-matching-overload] sphinx/search/__init__.py:497:36: No overload of function `fspath` matches arguments
- error[no-matching-overload] sphinx/util/__init__.py:33:12: No overload of function `fspath` matches arguments
- error[no-matching-overload] sphinx/util/docstrings.py:10:22: No overload of function `compile` matches arguments
- error[no-matching-overload] sphinx/util/i18n.py:320:17: No overload of function `splitext` matches arguments
- error[no-matching-overload] sphinx/util/i18n.py:320:17: No overload of function `splitext` matches arguments
- error[no-matching-overload] sphinx/util/i18n.py:320:17: No overload of function `splitext` matches arguments
- error[no-matching-overload] sphinx/util/inspect.py:981:25: No overload of function `unparse` matches arguments
- error[no-matching-overload] sphinx/util/inspect.py:995:18: No overload of function `unparse` matches arguments
- error[call-non-callable] sphinx/util/inventory.py:319:16: Method `__getitem__` of type `Overload[(key: SupportsIndex, /) -> Unknown, (key: slice[Any, Any, Any], /) -> tuple[Unknown, ...]]` is not callable on object of type `tuple[Unknown, Unknown, Unknown, Unknown]`
- error[no-matching-overload] sphinx/util/osutil.py:37:12: No overload of function `fspath` matches arguments
- error[no-matching-overload] sphinx/util/rst.py:28:17: No overload of function `compile` matches arguments
- Found 662 diagnostics
+ Found 646 diagnostics

rotki (https://github.com/rotki/rotki)
+ warning[unused-ignore-comment] rotkehlchen/externalapis/etherscan.py:443:63: Unused blanket `type: ignore` directive
+ warning[unused-ignore-comment] rotkehlchen/tests/db/test_history_events.py:268:46: Unused blanket `type: ignore` directive
- error[no-matching-overload] rotkehlchen/tests/unit/test_calendar_reminders.py:145:25: No overload of function `get_decoded_events_of_transaction` matches arguments
- Found 2043 diagnostics
+ Found 2044 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- error[no-matching-overload] tests/appsec/iast/aspects/aspect_utils.py:33:24: No overload of function `sub` matches arguments
- Found 6912 diagnostics
+ Found 6911 diagnostics

zulip (https://github.com/zulip/zulip)
- error[no-matching-overload] zerver/tests/test_slack_importer.py:101:26: No overload of function `urlsplit` matches arguments
- error[no-matching-overload] zerver/tornado/django_api.py:55:26: No overload of function `urlsplit` matches arguments
- error[no-matching-overload] zproject/config.py:59:12: No overload of function `get_config` matches arguments
- Found 6944 diagnostics
+ Found 6941 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- error[no-matching-overload] src/prefect/_internal/concurrency/calls.py:84:26: No overload of function `get_deadline` matches arguments
- error[no-matching-overload] src/prefect/_internal/schemas/validators.py:297:12: No overload of function `fspath` matches arguments
- error[no-matching-overload] src/prefect/client/schemas/actions.py:128:16: No overload of function `validate_schedule_max_scheduled_runs` matches arguments
- error[no-matching-overload] src/prefect/client/schemas/actions.py:196:16: No overload of function `validate_schedule_max_scheduled_runs` matches arguments
- error[no-matching-overload] src/prefect/client/schemas/objects.py:733:16: No overload of function `list_length_50_or_less` matches arguments
- error[no-matching-overload] src/prefect/client/schemas/objects.py:738:16: No overload of function `validate_not_negative` matches arguments
- error[no-matching-overload] src/prefect/client/schemas/objects.py:1594:16: No overload of function `validate_max_metadata_length` matches arguments
- error[no-matching-overload] src/prefect/client/schemas/schedules.py:139:16: No overload of function `default_timezone` matches arguments
- error[no-matching-overload] src/prefect/serializers.py:243:16: No overload of function `cast_type_names_to_serializers` matches arguments
- error[no-matching-overload] src/prefect/server/schemas/actions.py:521:16: No overload of function `validate_cache_key_length` matches arguments
- error[no-matching-overload] src/prefect/server/schemas/core.py:377:16: No overload of function `list_length_50_or_less` matches arguments
- error[no-matching-overload] src/prefect/server/schemas/core.py:382:16: No overload of function `validate_not_negative` matches arguments
- error[no-matching-overload] src/prefect/tasks.py:1067:16: No overload of function `run_task` matches arguments
- error[no-matching-overload] src/prefect/utilities/collections.py:401:20: No overload of function `visit_collection` matches arguments
- error[no-matching-overload] src/prefect/workers/base.py:140:16: No overload of function `return_v_or_none` matches arguments
- Found 4499 diagnostics
+ Found 4484 diagnostics

sympy (https://github.com/sympy/sympy)
- error[no-matching-overload] sympy/utilities/exceptions.py:271:13: No overload of function `warn_explicit` matches arguments
- Found 18573 diagnostics
+ Found 18572 diagnostics

scipy (https://github.com/scipy/scipy)
- error[no-matching-overload] scipy/_lib/_array_api.py:378:16: No overload of function `norm` matches arguments
- error[no-matching-overload] scipy/sparse/linalg/tests/test_norm.py:134:59: No overload of function `norm` matches arguments
- error[no-matching-overload] scipy/sparse/linalg/tests/test_norm.py:137:41: No overload of function `norm` matches arguments
- error[no-matching-overload] subprojects/highs/src/highspy/highs.py:1032:16: No overload of bound method `__init__` matches arguments
- error[no-matching-overload] subprojects/highs/src/highspy/highs.py:1182:16: No overload of bound method `__init__` matches arguments
- error[no-matching-overload] subprojects/highs/src/highspy/highs.py:1604:16: No overload of bound method `__init__` matches arguments
- error[no-matching-overload] subprojects/highs/src/highspy/highs.py:1905:78: No overload of bound method `__init__` matches arguments
- error[no-matching-overload] subprojects/highs/src/highspy/highs.py:1973:78: No overload of bound method `__init__` matches arguments
- error[no-matching-overload] subprojects/highs/src/highspy/highs.py:2065:16: No overload of bound method `__init__` matches arguments
- Found 7766 diagnostics
+ Found 7757 diagnostics

@flying-sheep

This comment was marked as resolved.

Comment on lines +128 to +135
/// Represents the state of the expansion process.
///
/// This is useful to avoid cloning the initial types vector if none of the types can be
/// expanded.
enum State<'a, 'db> {
Initial(&'a Vec<Type<'db>>),
Expanded(Vec<Vec<Type<'db>>>),
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mainly an optimization to avoid cloning the self.types vector to initialize the std::iter::successors iterator. This would be useful in cases where no argument types can be expanded.

Either::Right(std::iter::once(element))
}
})
.multi_cartesian_product()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can avoid using itertools here and have a custom implementation for this but it would also require allocation so I thought that we might as well utilize an existing implementation.

Comment on lines 1214 to 1253
if return_types.len() == expanded_argument_lists.len() {
// If the number of return types is equal to the number of expanded argument lists,
// they all evaluated successfully. So, we need to combine their return types by
// union to determine the final return type.
//
// TODO: What should be the state of the bindings at this point?
self.return_type = Some(UnionType::from_elements(db, return_types));
return;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to spend some time thinking about this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will require some kind of "merge" logic to make sure the bindings state is correct at the end of a successful argument lists evaluation.

Consider the following abstract example which contains 4 overloads:

@overload
def f(...) -> A: ...
@overload
def f(...) -> B: ...
@overload
def f(...) -> C: ...
@overload
def f(...) -> D: ...

After expanding the first argument, we get two argument lists which matches overload 1 and 3 respectively. This means that we now have the winning overload matches (1 and 3), so the return type would be A | C.

The logic in this PR is such that it restores the state of every binding after evaluating a single argument list, so at the end of evaluating the two argument lists (as stated above), the state would be as if we didn't perform type checking at all. I think this is incorrect.

In Binding, there are 6 fields which we need to consider that gets updated during type checking which is what the BindingSnapshot is recording:

struct BindingSnapshot<'db> {
    return_ty: Type<'db>,
    specialization: Option<Specialization<'db>>,
    inherited_specialization: Option<Specialization<'db>>,
    argument_parameters: Box<[Option<usize>]>,
    parameter_tys: Box<[Option<Type<'db>>]>,
    errors_position: usize,
}

Here, it's the errors field that represents whether an overload was "matched" or not while other fields represent additional information that type checking revealed.

Coming back to the above abstract example:

  1. For overload 1, the first argument list evaluated successfully
  2. For overload 3, the second argument list evaluated successfully
  3. For overload 2 and 4, none of the argument lists matched

For (1) and (2), the errors field for the Binding corresponding to the matched overload should be empty and other fields should have the information from the successful evaluation.

For (3), I'm not exactly sure what would be the correct approach. My thinking is that:

  • Combine all the errors from evaluating both argument lists
  • Choose the return_ty from the last argument list
  • Combine (?) the specialization and inherited_specialization
  • Choose the argument_parameters from the last argument list
  • Choose the parameter_tys from the last argument list

Quickly looking at what Pyright does, it seems that it re-evaluates it using the first argument list: https://github.com/microsoft/pyright/blob/321b6bf687c6c3ffa3eb627aeb8a143bc4740cde/packages/pyright-internal/src/analyzer/typeEvaluator.ts#L9340-L9370

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For (3), the updated merged state would be something like the following:

    initial     arglist 1    arglist 2    final
    --------    ---------    ---------    -----
1   unmatched   matched      unmatched    matched
2   unmatched   unmatched    matched      matched
3   unmatched   unmatched    unmatched    unmatched

The "initial" is the state of each overloads before performing argument type expansion i.e., all three overloads are unmatched.

After evaluating the first argument list, it resulted in overload 1 being matched. Similarly, after evaluating the second argument list, it resulted in overload 2 being matched.

So, the final state of the bindings would indicate the overload 1 and 2 are matched while overload 3 is unmatched. This means that information about parameter types and specializations for the matched overload should be from the argument list that resulted in the match.

And, the return type would be the union of the "matched" overloads. This is a successful evaluation because both argument list resulted in a single matched overload.

Comment on lines 1143 to 1173
let pre_evaluation_snapshot = snapshot(self);

// Step 2: Evaluate each remaining overload as a regular (non-overloaded) call to determine
// whether it is compatible with the supplied argument list.
for (_, overload) in self.matching_overloads_mut() {
overload.check_types(db, argument_types.as_ref(), argument_types.types());
}

match self.matching_overload_index() {
MatchingOverloadIndex::None => {
// If all overloads result in errors, proceed to step 3.
}
MatchingOverloadIndex::Single(_) => {
// If only one overload evaluates without error, it is the winning match.
return;
}
MatchingOverloadIndex::Multiple(_) => {
// If two or more candidate overloads remain, proceed to step 4.
// TODO: Step 4 and Step 5 goes here...
// We're returning here because this shouldn't lead to argument type expansion.
return;
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a possibility that this can be merged into a single loop that would also consider the expanded argument lists but for now I decided to just duplicate this part as it isn't too complex.

@dhruvmanila
Copy link
Member Author

I think there are few more cases to test and I also need to think a bit more around the bindings state at the end of a successful evaluation of an expanded argument lists but I'm marking this as ready for review to get some initial feedback, specifically on the structure of the implementation and if there's something obvious that I've missed or mis-interpreted based on my reading of the spec.

@dhruvmanila dhruvmanila marked this pull request as ready for review May 30, 2025 16:25
Copy link
Member

@dcreager dcreager left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stepping out to pick up my son from school, here are some initial comments

/// contains the same arguments, but with one or more of the argument types expanded.
///
/// [argument type expansion]: https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion
pub(crate) fn expand(&self, db: &'db dyn Db) -> impl Iterator<Item = Vec<Vec<Type<'db>>>> + '_ {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this implementation!

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just glanced quickly at the tests for semantics; looks like @dcreager already has it covered for detailed review of the code. This is fantastic, thank you!

@dhruvmanila dhruvmanila force-pushed the dhruv/argument-type-expansion branch from a44db26 to fc725c2 Compare June 2, 2025 10:35
Comment on lines +1131 to +1139
// TODO: Evaluate it as a regular (non-overloaded) call. This means that any
// diagnostics reported in this check should be reported directly instead of
// reporting it as `no-matching-overload`.
self.overloads[index].check_types(
db,
argument_types.as_ref(),
argument_types.types(),
);
return;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO is important because we currently give no-matching-overload which is incorrect because we did match an overload, it's type checking that failed.

@dhruvmanila dhruvmanila requested a review from carljm June 3, 2025 04:33
@dhruvmanila dhruvmanila marked this pull request as draft June 3, 2025 14:48
@dhruvmanila
Copy link
Member Author

I've put this in draft because I need to make some small tweaks with respect to the final state of the bindings that I've realized after talking with Carl in our 1:1 now. I'll quickly make those changes and will ask for review. Apologies if anyone was already looking at this PR.

@dhruvmanila dhruvmanila force-pushed the dhruv/argument-type-expansion branch from 5ac3b38 to 5d2614c Compare June 3, 2025 15:40
@dhruvmanila
Copy link
Member Author

dhruvmanila commented Jun 3, 2025

Ok, this is ready for a final review. I've commented here to visualize what the final state of the bindings look like. Happy to answer any questions that you might have. (cc @dcreager @carljm)

@dhruvmanila dhruvmanila marked this pull request as ready for review June 3, 2025 15:43
@dcreager
Copy link
Member

dcreager commented Jun 3, 2025

Ok, this is ready for a final review

I'm heading into an interview but will try to take a look at this later today

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Copy link
Member

@dcreager dcreager left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one last nit otherwise this looks great!

@dhruvmanila dhruvmanila enabled auto-merge (squash) June 4, 2025 02:09
@dhruvmanila dhruvmanila merged commit 7ea773d into main Jun 4, 2025
32 checks passed
@dhruvmanila dhruvmanila deleted the dhruv/argument-type-expansion branch June 4, 2025 02:12
carljm added a commit to mtshiba/ruff that referenced this pull request Jun 4, 2025
* main:
  [ty] Add tests for empty list/tuple unpacking (astral-sh#18451)
  [ty] Argument type expansion for overload call evaluation (astral-sh#18382)
  [ty] Minor cleanup for `site-packages` discovery logic (astral-sh#18446)
  [ty] Add generic inference for dataclasses (astral-sh#18443)
  [ty] dataclasses: Allow using dataclasses.dataclass as a function. (astral-sh#18440)
  [ty] Create separate `FunctionLiteral` and `FunctionType` types (astral-sh#18360)
  [ty] Infer `list[T]` when unpacking non-tuple type (astral-sh#18438)
  [ty] Meta-type of type variables should be type[..] (astral-sh#18439)
  [`pyupgrade`] Make fix unsafe if it deletes comments (`UP050`) (astral-sh#18390)
  [`pyupgrade`] Make fix unsafe if it deletes comments (`UP004`) (astral-sh#18393)
  [ty] Support using legacy typing aliases for generic classes in type annotations (astral-sh#18404)
  Use ty's completions in playground (astral-sh#18425)
  Update editor setup docs about Neovim and Vim (astral-sh#18324)
  Update NPM Development dependencies (astral-sh#18423)
@flying-sheep
Copy link
Contributor

flying-sheep commented Jun 4, 2025

seems like this didn’t suffice to make the test case above work:

https://play.ty.dev/093e1321-5e0f-4b1c-b46e-26745c902c1d

Object of type MyTupleLike is not iterable (not-iterable) [Ln 34, Col 8]

maybe another part of overload evaluation is responsible here. Should I file an issue?

@dhruvmanila
Copy link
Member Author

I'll need to look at your example a bit closely to see what you're trying to do but you're correct that this step of the algorithm wouldn't have solved it. Apologies for not noticing it earlier as this step is performing argument type expansion and in your example overload, the argument type is just int. For reference, other steps of the algorithm are being tracked as sub-issues over at astral-sh/ty#104.

@flying-sheep
Copy link
Contributor

flying-sheep commented Jun 4, 2025

The issue in my example is that ty knows that a class instance is iterable if it has a __getattr__ __getitem__ method that can be called with a single positional argument of type int. The issue with my example is that this is the case, but ty doesn’t understand that the overloaded method satisfies that interface.

@AlexWaygood
Copy link
Member

if it has a __getattr__ method

(I think you mean __getitem__ :-)

@flying-sheep
Copy link
Contributor

flying-sheep commented Jun 4, 2025

Here’s a (failing) mdtest case:

### Filling niches

`overloaded.pyi`:

```pyi
from typing import Literal, overload
from ty_extensions import Not, Intersection

class A: ...
class B: ...

@overload
def f(x: Literal[0]) -> A: ...
@overload
def f(x: Intersection[int, Not[Literal[0]]]) -> B: ...
```

```py
from overloaded import A, B, f

def _(x: int):
    reveal_type(f(0))  # revealed: A
    reveal_type(f(1))  # revealed: B
    reveal_type(f(x))  # revealed: A | B
```

@carljm
Copy link
Contributor

carljm commented Jun 4, 2025

@flying-sheep You're right that a type checker could expand int to a union of literal integer types and support this. It's not currently part of the typing spec to do this, and no other type checker does it, but it could certainly be done. It's a bit trickier than the currently specified expansions because it's an infinite expansion, but I suspect practically limiting it to some small integer types would work fine in practice. (Though sooner or later someone would come along and wonder why it works for small integers but not large ones... maybe it would be possible to actually inspect the overloaded function and see which literal int types appear to be present in the overload signatures?)

I think we have higher priority things we need to do at the moment, though. It seems fairly easy to work around this issue: is there any downside to just adding an explicit third overload def f(x: int) -> A | B: ...? That seems to be enough to make it behave the way you want.

@flying-sheep
Copy link
Contributor

flying-sheep commented Jun 4, 2025

If I read steps 3 of the “overload call evaluation” part of the spec correctly, that behavior is wrong:

If all argument lists evaluate successfully, combine their respective return types by union to determine the final return type for the call, and stop.

So since int overlaps with Literal[0], given your definition, when passing 0, f(0) should evaluate to A|B.

Or am I wrong? PyRight complains when overlapping overloads like this exist

@carljm
Copy link
Contributor

carljm commented Jun 4, 2025

So since int overlaps with Literal[0], given your definition, when passing 0, f(0) should evaluate to A|B.

That's not how overload evaluation is specified to work in Python. The first matching overload is used, not the union of the return types of all matching overloads.

(I personally think it would be better to treat overloads as an un-ordered intersection of signatures, and union the return types of all matched overloads, but we actually tried this in ty and the results from the ecosystem made it clear that it would be too incompatible with existing usage. Which isn't too surprising, given that the existing ecosystem doesn't have negation types available, and you really need negation types to make the "un-ordered intersection of signatures" interpretation usable. The ordered behavior effectively gives you implicit negation by having a more specific overload shadow a more general one.)

@flying-sheep
Copy link
Contributor

flying-sheep commented Jun 4, 2025

OK, is there some explicit mention of that in the spec? The part I’m quoting seems to contradict that (at least in the presence of expandable unions), while Step 6 agrees with you.

So PyRight (and maybe mypy) are just buggy?

PS: my original motivation is “simply” being able to type unpacking a heterogeneous collection: https://play.ty.dev/9817830f-c7c1-4a44-b4be-91704fe9500d

adding that overload will of course make that fail

@carljm
Copy link
Contributor

carljm commented Jun 4, 2025

You need to consider the whole algorithm as presented in the spec, for any given case. Note that union expansion doesn't happen at all, unless zero overloads match after step 2.

If two or more candidate overloads remain, proceed to step 4.

In the case of passing 0 with the three-overload version here, two overloads match after step 2, which means we go directly to step 4, bypassing step 3. We end up reaching step 6 and choosing the first matching overload. The text you quoted about merging return types only applies when trying multiple different union expansions.

So PyRight (and maybe mypy) are just buggy?

I'm not sure which behavior you are referring to as buggy here.

adding that overload will of course make that fail

I don't think so? It works fine for direct indexing: https://play.ty.dev/01df94d6-0b7e-4df9-9d85-f7d4dbc92224

It doesn't work for iteration/unpacking because we simply model a call to __getitem__ with an argument of type int in order to determine the type returned by iterating using the legacy iteration protocol, we don't model it as calling with a sequence of literal integer types. This is a potential improvement we could make in the future, at least in the unpacking scenario.

@flying-sheep
Copy link
Contributor

flying-sheep commented Jun 4, 2025

Thanks for walking me through the algorithm, I wasn’t sure if I missed something!

It works fine for direct indexing

yeah, which is why I specified that what I care about here is the unpacking lol

I'm not sure which behavior you are referring to as buggy here.

See https://microsoft.github.io/pyright/#/configuration?id=type-check-rule-overrides

reportOverlappingOverload [boolean or string, optional]: Generate or suppress diagnostics for function overloads that overlap in signature and obscure each other or have incompatible return types. The default value for this setting is "error".

You’re saying that the spec says that overlapping overloads are an intentional part of the spec. PyRight flags them as error unless you ignore that. Should I report a bug?

@carljm
Copy link
Contributor

carljm commented Jun 4, 2025

You’re saying that the spec says that overlapping overloads are an intentional part of the spec. PyRight flags them as error unless you ignore that. Should I report a bug?

No; the second part "have incompatible return types" is critical there. The spec says that overlapping overloads are fine, as long as the later overload has a return type that is a super-type of the earlier overload. (This is what's necessary to make the overlapping overloads sound in the face of uncertain precision of the argument type.) That's the same logic that pyright's warning implements.

@flying-sheep
Copy link
Contributor

flying-sheep commented Jun 4, 2025

OK, so we’re at square 1:

Currently, the only way to have a chance at correctly typing the unpacking of a heterogeneous collection is to

  1. do exactly what I’m doing in https://play.ty.dev/9817830f-c7c1-4a44-b4be-91704fe9500d (i.e. do not add the int overload)
  2. add support for that to type checkers

right?

@carljm
Copy link
Contributor

carljm commented Jun 4, 2025

I don't understand why (1) is required, or how the extra overload causes a problem. (It meets the requirement I mentioned that its return type is a super type of the prior overlapping overload return types.) My link above adds the extra int overload, and direct indexing works as expected. So it seems to me that all that is needed is (2) for us to model the legacy iteration protocol as a sequence of calls with literal ints, rather than as a single call with int argument.

@flying-sheep
Copy link
Contributor

Yeah, you’re right! That instead of “it’s an iterable over T, so unpacking yields a bunch of Ts”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement argument type expansion for overload call evaluation

6 participants