Endless call cycle between is_callable_compatible and is_protocol_implementation

未关闭
#21,739 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
42/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
冷清
技术栈
python

调研方向

从精简的 Python reproducer 和 mypy/subtypes.py 中的 traceback 开始,重点关注 is_callable_compatible、is_protocol_implementation 以及它们通过 types.py 发起的调用。使用 --show-traceback -v 重现挂起,然后跟踪递归的子类型检查。当示例以预期结果结束,并且回归覆盖阻止无限调用循环时,即表示完成。

由索引模型根据 Issue 内容生成。

描述

bug crash topic-protocols topic-recursive-types

Discovered by @glyph and I see it's not his first rodeo (https://github.com/python/mypy/issues/18195 for instance).

My best interpretation (which could be wrong) is that a generic class which references itself in a method's type annotation, when typed using a protocol, will cause mypy to loop endlessly between matching the protocol and checking the types in the class. I haven't pinpointed the exact mechanism but it kind of makes sense that this could happen.

To Reproduce

The original code is here: https://github.com/glyph/OmniFocusStreamCounter/commit/454d0727363595b770cb150cc08a0018bc04a783

But this is about as far as I could reduce it:

from dataclasses import dataclass
from typing import Any, Callable, Protocol


class PExp[T](Protocol):
    def EQ(self, other: T) -> PExp[T]: ...


class PRef[T](Protocol):
    def EQ(self, other: T) -> PExp[T]: ...


@dataclass
class Exp[A, B]:
    _left: A
    _op: str
    _right: B

    def EQ(self, other: Any) -> Exp[Exp[A, B], Any]:
        return Exp(self, "EQ", other)


@dataclass
class Ref[T]:
    get: Callable[[], T]

    def EQ(self, other: T) -> Exp[Ref[T], T]:
        return Exp(self, "EQ", other)


@dataclass
class Cache:
    def spam(self, name: str) -> PRef[str]:
        return Ref(lambda: "spam")

Expected Behavior

So, if you make Exp.EQ untyped, then mypy will succeed (I fixed some problems in the original code 😉 ) This is obviously the expected behaviour.

Actual Behavior

Running mypy from HEAD, uncompiled, with --show-traceback - it hangs, then on Ctrl-C we see, this, endlessly:

  File "/home/dhd/work/mypy/mypy/subtypes.py", line 1281, in is_protocol_implementation                                                              07:44:34 [233/1846]
    is_compat = is_subtype(                                                                                                                                             
        subtype, supertype, ignore_pos_arg_names=ignore_names, options=options                                                                                          
    )                                                                                                                                                                   
  File "/home/dhd/work/mypy/mypy/subtypes.py", line 189, in is_subtype                                                                                                  
    return _is_subtype(left, right, subtype_context, proper_subtype=False)                                                                                              
  File "/home/dhd/work/mypy/mypy/subtypes.py", line 376, in _is_subtype                                                                                                 
    return left.accept(SubtypeVisitor(orig_right, subtype_context, proper_subtype))
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dhd/work/mypy/mypy/types.py", line 2361, in accept
    return visitor.visit_callable_type(self)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/home/dhd/work/mypy/mypy/subtypes.py", line 740, in visit_callable_type
    return is_callable_compatible(
        left,
    ...<8 lines>...
        ),
    )
  File "/home/dhd/work/mypy/mypy/subtypes.py", line 1785, in is_callable_compatible
    if not ignore_return and not is_compat_return(left.ret_type, right.ret_type):
                                 ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dhd/work/mypy/mypy/subtypes.py", line 448, in _is_subtype
    return is_subtype(left, right, subtype_context=self.subtype_context)
  File "/home/dhd/work/mypy/mypy/subtypes.py", line 189, in is_subtype
    return _is_subtype(left, right, subtype_context, proper_subtype=False)
  File "/home/dhd/work/mypy/mypy/subtypes.py", line 376, in _is_subtype
    return left.accept(SubtypeVisitor(orig_right, subtype_context, proper_subtype))
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dhd/work/mypy/mypy/types.py", line 1684, in accept
    return visitor.visit_instance(self)
           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/home/dhd/work/mypy/mypy/subtypes.py", line 627, in visit_instance
    if right.type.is_protocol and is_protocol_implementation(
                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~^
        left, right, proper_subtype=self.proper_subtype, options=self.options
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ):
    ^

Your Environment

  • Mypy version used: mypy 2.4.0+dev.67eecf07fea3fa4bbaab9d46cd28372a8ded330b (compiled: no)
  • Mypy command-line flags: --show-traceback -v
  • Mypy configuration options from mypy.ini (and other config files):
[tool.mypy]
namespace_packages = true
plugins = ["mypy_zope:plugin"]

[[tool.mypy.overrides]]
ignore_missing_imports = true
module = [
  "objc",
  "Foundation",
  "AppKit",
  "ServiceManagement",
  "setuptools",
  "appscript",
]
  • Python version used: 3.14
主要语言
Python
星标
20.6k
派生
3.3k
平均合并
1 天 18 小时
30 天内合并 PR
54

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

python/mypy 的其他 Issue

查看 python/mypy 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。