Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0332484
feat: report `@deprecated()` on non-overloaded class constructors
bzoracler Oct 23, 2025
d497f37
Add simple subclass tests
bzoracler Oct 26, 2025
53622b1
fix error message typo
bzoracler Oct 26, 2025
39cd5f9
Merge branch 'python:master' into fix-20103
bzoracler Oct 27, 2025
a473e52
Save an unnecessary instance check
bzoracler Oct 27, 2025
8f08c60
Use the constructor definition chosen by mypy rather than walking 2 MROs
bzoracler Oct 27, 2025
b0ffd6b
Warn deprecated constructor calls from old-style type aliases
bzoracler Oct 27, 2025
c954f78
fix unexpanded type check error
bzoracler Oct 27, 2025
00897ab
fix unexpanded type check error
bzoracler Oct 27, 2025
3b7b986
augment basic deprecation tests with override checks
bzoracler Oct 30, 2025
4b42ea6
add specific tests for deprecated overloaded class constructors
bzoracler Oct 30, 2025
9e49aa1
add test for accessing methods via `self` and `cls`
bzoracler Oct 30, 2025
4be62c6
add tests for accessing class constructor methods directly
bzoracler Oct 30, 2025
79d0cb0
implement reporting on `type[ClassWithDeprecatedConstructor]()`
bzoracler Oct 30, 2025
5302af3
add incremental test for calling deprecated constructors
bzoracler Oct 30, 2025
d595948
add tests for unioned type calls to deprecated constructors
bzoracler Oct 30, 2025
c7c580c
fix reversion error
bzoracler Nov 1, 2025
f02dc8c
fix reversion error
bzoracler Nov 1, 2025
9cfdf7a
Restore original formatting
bzoracler Nov 1, 2025
7c0b5e3
Use `int()` instead of custom bool function
bzoracler Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1420,7 +1420,11 @@ def is_generic_decorator_overload_call(
return None

def handle_decorator_overload_call(
self, callee_type: CallableType, overloaded: Overloaded, ctx: Context
self,
callee_type: CallableType,
overloaded: Overloaded,
ctx: Context,
callee_is_overload_item: bool,
) -> tuple[Type, Type] | None:
"""Type-check application of a generic callable to an overload.

Expand All @@ -1432,7 +1436,9 @@ def handle_decorator_overload_call(
for item in overloaded.items:
arg = TempNode(typ=item)
with self.msg.filter_errors() as err:
item_result, inferred_arg = self.check_call(callee_type, [arg], [ARG_POS], ctx)
item_result, inferred_arg = self.check_call(
callee_type, [arg], [ARG_POS], ctx, is_overload_item=callee_is_overload_item
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

callee_is_overload_item in this method doesn't actually activate any code paths. Its addition is for logical consistency and future-proofing any more cases handled here:

mypy/mypy/checkexpr.py

Lines 1403 to 1415 in 054f721

def is_generic_decorator_overload_call(
self, callee_type: CallableType, args: list[Expression]
) -> Overloaded | None:
"""Check if this looks like an application of a generic function to overload argument."""
assert callee_type.variables
if len(callee_type.arg_types) != 1 or len(args) != 1:
# TODO: can we handle more general cases?
return None
if not isinstance(get_proper_type(callee_type.arg_types[0]), CallableType):
return None
if not isinstance(get_proper_type(callee_type.ret_type), CallableType):
return None
with self.chk.local_type_map:

The test for future-proofing is this one:

[case testDeprecatedOverloadedClassConstructorDecoratingOverloadedFunction]

Copy link
Collaborator

Choose a reason for hiding this comment

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

A cannot come up with a case where callee_is_overload_item would actually be required. Could you please give an example?

Copy link
Contributor Author

@bzoracler bzoracler Nov 1, 2025

Choose a reason for hiding this comment

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

This would be required if mypy supported the following:

from typing import Callable, Generic, TypeVar
from typing_extensions import deprecated, overload

F = TypeVar("F", bound=Callable[..., object])

class OverloadWrapper(Generic[F]):
    __call__: F

    @overload
    @deprecated("decorated function must take arguments")
    def __init__(self: OverloadWrapper[Callable[[], None]], f: Callable[[], None]) -> None: ...
    @overload
    def __init__(self, f: F) -> None: ...

@overload
def f(a: str) -> None: ...
@overload
def f() -> None: ...
# Currently: Revealed type is "__main__.OverloadWrapper[def ()]"  with a deprecation warning
# More ideally: Revealed type is "Overload(def (a: builtins.str), def ())" with a deprecation warning
# Without `callee_is_overload_item`, the ideal case would reveal "Overload(def (a: builtins.str), def ())" but the deprecation warning wouldn't be emitted.
reveal_type(OverloadWrapper(f)))

That is, wrapping an overload with a class (rather than a function) while preserving or transforming its signature.

The change was made in anticipation that the ideal case should be supported some day, and wouldn't let a potential deprecation report slip by. However, it may be a bit premature on my part?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This use case seems very complicated, and I am not sure deducing Overload instead of OverloadWrapper would be ideal. Additional members of OverloadWrapper would then be unknown to Mypy, wouldn't they?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there already a request for such a feature?

if err.has_new_errors():
# This overload doesn't match.
continue
Expand Down Expand Up @@ -1538,6 +1544,7 @@ def check_call(
callable_name: str | None = None,
object_type: Type | None = None,
original_type: Type | None = None,
is_overload_item: bool = False,
) -> tuple[Type, Type]:
"""Type check a call.

Expand All @@ -1558,6 +1565,7 @@ def check_call(
or None if unavailable (examples: 'builtins.open', 'typing.Mapping.get')
object_type: If callable_name refers to a method, the type of the object
on which the method is being called
is_overload_item: Whether this check is for an individual overload item
"""
callee = get_proper_type(callee)

Expand All @@ -1568,7 +1576,7 @@ def check_call(
# Special casing for inline application of generic callables to overloads.
# Supporting general case would be tricky, but this should cover 95% of cases.
overloaded_result = self.handle_decorator_overload_call(
callee, overloaded, context
callee, overloaded, context, is_overload_item
)
if overloaded_result is not None:
return overloaded_result
Expand All @@ -1582,6 +1590,7 @@ def check_call(
callable_node,
callable_name,
object_type,
is_overload_item,
)
elif isinstance(callee, Overloaded):
return self.check_overload_call(
Expand Down Expand Up @@ -1659,11 +1668,18 @@ def check_callable_call(
callable_node: Expression | None,
callable_name: str | None,
object_type: Type | None,
is_overload_item: bool = False,
) -> tuple[Type, Type]:
"""Type check a call that targets a callable value.

See the docstring of check_call for more information.
"""
# Check implicit calls to deprecated class constructors.
# Only the non-overload case is handled here. Overloaded constructors are handled
# separately during overload resolution.
if (not is_overload_item) and callee.is_type_obj():
self.chk.warn_deprecated(callee.definition, context)
Comment on lines +1680 to +1681
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks very nice now!

Maybe a little bit more readable: callee.is_type_obj() and not is_overloaded_item.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I didn't initially want to sprinkle an extra method parameter everywhere, but was necessary for supporting type[ClassWithDeprecatedConstructor]().

Maybe a little bit more readable

I wouldn't usually write it this way. Here, the majority of time that check_callable_call is called, is_overload_item is False, and checking booleans is much cheaper than the method call callee.is_type_obj(). This was an easy short-circuit which I thought wouldn't impact readability with the parentheses added around (not is_overload_item).


# Always unpack **kwargs before checking a call.
callee = callee.with_unpacked_kwargs().with_normalized_var_args()
if callable_name is None and callee.name:
Expand Down Expand Up @@ -2910,6 +2926,7 @@ def infer_overload_return_type(
context=context,
callable_name=callable_name,
object_type=object_type,
is_overload_item=True,
)
is_match = not w.has_new_errors()
if is_match:
Expand Down
Loading