Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mypy hangs on recursive type keyword in combination with an unresolved type name #18505

Open
NiklasRosenstein opened this issue Jan 22, 2025 · 2 comments
Labels
crash topic-pep-695 Issues related to PEP 695 syntax

Comments

@NiklasRosenstein
Copy link

NiklasRosenstein commented Jan 22, 2025

Bug Report

From as far as I could boil it down, it seems that when you use the type keyword in one place referencing a type name that is undefined, and then using a another type keyword to re-assign the same global name on a class member, Mypy freezes.

To Reproduce

from typing import TypedDict

type Resource = dict[str, Any]

class CustomizeRequest: ...

class DecoratorController:
    type CustomizeRequest = CustomizeRequest

https://mypy-play.net/?mypy=latest&python=3.12&gist=f69f8d424122ed4c7d0778b37e34021f

It begins working as soon as you do any one of these things:

  • from typing import Any (passes)
  • type SomeOtherName = CustomizeRequest (fails, Any undefined)
  • remove type Resource = dict[str, Any] (passes)

Expected Behavior

Mypy should not freeze, but error and report that Any is undefined.

Actual Behavior

As explained above.

Your Environment

  • Mypy version used: 1.14.1 (compiled: yes)
  • Mypy command-line flags: n/a
  • Mypy configuration options from mypy.ini (and other config files): strict = true
  • Python version used: CPython 3.13.1
@NiklasRosenstein NiklasRosenstein added the bug mypy got something wrong label Jan 22, 2025
@NiklasRosenstein
Copy link
Author

NiklasRosenstein commented Jan 22, 2025

Hold on, it seems to also happen not necessarily because of an undefined type name. In my original file, the one from which I started stripping down to a smaller reproducible version, I don't actually have an undefined type name. Simply lifting the recursive referencing of the type lines makes it pass.

Maybe the way I want to use the type keyword is not how it is intended to work (although it would be in line with how attribute assignments work). Regardless, Mypy shouldn't freeze.

Full example: https://mypy-play.net/?mypy=latest&python=3.12&gist=cb3570afdf70f2a8117c3b680b275f04

Diff to make it pass instead of freeze:

diff --git a/applicationmapper/metacontroller_api.py b/applicationmapper/metacontroller_api.py
index d52185d..7b1f931 100644
--- a/applicationmapper/metacontroller_api.py
+++ b/applicationmapper/metacontroller_api.py
@@ -223,8 +223,8 @@ class ResourceRule(TypedDict):
 
 
 class DecoratorController:
-    type CustomizeRequest = CustomizeRequest
-    type CustomizeResponse = CustomizeResponse
+    type CustomizeRequestFoo = CustomizeRequest
+    type CustomizeResponseFoo = CustomizeResponse
     type SyncRequest = DecoratorSyncRequest.Type
     type SyncResponse = DecoratorSyncResponse.Type
     type FinalizeRequest = DecoratorFinalizeRequest.Type
@@ -244,8 +244,8 @@ class DecoratorController:
 
 
 class CompositeController:
-    type CustomizeRequest = CustomizeRequest
-    type CustomizeResponse = CustomizeResponse
+    type CustomizeRequestFoo = CustomizeRequest
+    type CustomizeResponseFoo = CustomizeResponse
     type SyncRequest = CompositeSyncRequest.Type
     type SyncResponse = CompositeSyncResponse.Type
     type FinalizeRequest = CompositeFinalizeRequest.Type

Also using CustomizeRequest: TypeAlias = CustomizeRequest works as expected

Edit: What's also interesting is that I actually want to use

    type: CustomizeRequest = CustomizeRequest.Type

in which case Mypy will not freeze, but it will complain that the name is not defined (in either case, using the type keyword and TypeVar annotation). The only workaround I found for this so far is

class CustomizeRequest:
    class Type(TypedDict): ...
type: _CustomizeRequest = CustomizeRequest.Type
class CompositeController:
    type CustomizeRequest = _CustomizeRequest

@sterliakov
Copy link
Collaborator

sterliakov commented Jan 22, 2025

The most minimal repro I managed to construct (without typeddict and other type statements):

class CustomizeResponse:
    relatedResources: "ResourceRule"

class ResourceRule: pass

class DecoratorController:
    type CustomizeResponse = CustomizeResponse

playground

A few random interruptions explain where mypy is stuck: it is busy expanding the alias.

Traceback (most recent call last):
  File "/home/stas/Documents/Work/mypy/mypy/__main__.py", line 15, in console_entry
    main()
  File "/home/stas/Documents/Work/mypy/mypy/main.py", line 119, in main
    res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/main.py", line 203, in run_build
    res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/build.py", line 191, in build
    result = _build(
             ^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/build.py", line 267, in _build
    graph = dispatch(sources, manager, stdout)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/build.py", line 2935, in dispatch
    process_graph(graph, manager)
  File "/home/stas/Documents/Work/mypy/mypy/build.py", line 3333, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/home/stas/Documents/Work/mypy/mypy/build.py", line 3428, in process_stale_scc
    mypy.semanal_main.semantic_analysis_for_scc(graph, scc, manager.errors)
  File "/home/stas/Documents/Work/mypy/mypy/semanal_main.py", line 93, in semantic_analysis_for_scc
    process_top_levels(graph, scc, patches)
  File "/home/stas/Documents/Work/mypy/mypy/semanal_main.py", line 220, in process_top_levels
    deferred, incomplete, progress = semantic_analyze_target(
                                     ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/semanal_main.py", line 351, in semantic_analyze_target
    analyzer.refresh_partial(
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 646, in refresh_partial
    self.refresh_top_level(node)
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 657, in refresh_top_level
    self.accept(d)
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 7247, in accept
    node.accept(self)
  File "/home/stas/Documents/Work/mypy/mypy/nodes.py", line 1177, in accept
    return visitor.visit_class_def(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 1728, in visit_class_def
    self.analyze_class(defn)
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 1944, in analyze_class
    self.analyze_class_body_common(defn)
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 1990, in analyze_class_body_common
    defn.defs.accept(self)
  File "/home/stas/Documents/Work/mypy/mypy/nodes.py", line 1258, in accept
    return visitor.visit_block(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 5261, in visit_block
    self.accept(s)
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 7247, in accept
    node.accept(self)
  File "/home/stas/Documents/Work/mypy/mypy/nodes.py", line 1665, in accept
    return visitor.visit_type_alias_stmt(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 5587, in visit_type_alias_stmt
    self.disable_invalid_recursive_aliases(s, current_node, s.value)
  File "/home/stas/Documents/Work/mypy/mypy/semanal.py", line 4213, in disable_invalid_recursive_aliases
    "tuple" if isinstance(get_proper_type(current_node.target), TupleType) else "union"
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/types.py", line 3197, in get_proper_type
    typ = typ._expand_once()
          ^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/types.py", line 347, in _expand_once
    new_tp = self.alias.target.accept(InstantiateAliasVisitor(mapping))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/types.py", line 405, in accept
    return visitor.visit_type_alias_type(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/stas/Documents/Work/mypy/mypy/expandtype.py", line 506, in visit_type_alias_type
    def visit_type_alias_type(self, t: TypeAliasType) -> Type:
    
KeyboardInterrupt

I'm not really deep into PEP695, what does it say about scoping here? My quick runtime checking says that alias' __value__ does not resolve to outer CustomizeResponse, so this alias doesn't seem to work as intended, but anyway freezing mypy completely isn't great.

(marking as crash since freezing is no better than traceback, it's a hard failure)

@sterliakov sterliakov added crash topic-pep-695 Issues related to PEP 695 syntax and removed bug mypy got something wrong labels Jan 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
crash topic-pep-695 Issues related to PEP 695 syntax
Projects
None yet
Development

No branches or pull requests

2 participants