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

Support recursive NamedTuple #9397

Closed
habilain opened this issue Sep 2, 2020 · 5 comments
Closed

Support recursive NamedTuple #9397

habilain opened this issue Sep 2, 2020 · 5 comments
Labels
bug mypy got something wrong semantic-analyzer Problems that happen during semantic analysis topic-named-tuple topic-recursive-types

Comments

@habilain
Copy link

habilain commented Sep 2, 2020

🐛 Bug Report

NamedTuple's appear to behave in unexpected ways with recursion, especially when compared to frozen dataclasses (which are rather similar, as far as the user is concerned).

To Reproduce

from __future__ import annotations
from typing import NamedTuple, Optional

class A(NamedTuple):
  a: Optional[A]

Produces a "mypy-sample1.py:5: error: Cannot resolve name "A" (possible cyclic definition)". However,

from __future__ import annotations
from typing import Optional
from dataclasses import dataclass

@dataclass(frozen=True)
class A:
  a: Optional[A]

Passes MyPy just fine, and as far as a user is concerned, presents a very similar surface for MyPy to type check (i.e. both have a read-only attribute a which is either None or of class A).

Expected Behavior

The expected behaviour would be that both NamedTuple and frozen dataclass behave the same, as NamedTuple and frozen dataclasses present logically similar behaviour with regards to the attribute 'a'.

Actual Behavior

In addition to the odd behaviour with cycle checker triggering on NamedTuple but not on dataclasses, it's possible to fool the cycle checker completely and have MyPy crash by recursing too far (e.g. issue #8695 and although that issue has been reported with a recursive type that could never be instantiated, I've encountered the same behaviour with type definitions that can be instantiated, but not in a minimal example friendly format). I think this specific behaviour is the root cause, as the real problem is that for some reason NamedTuple's are behaving differently to what would be expected.

Your Environment

  • Mypy version used: mypy 0.782
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.8.2
  • Operating system and version: Linux
@ramalho
Copy link

ramalho commented Jul 16, 2021

To illustrate the bug with a more "realistic" example, here is the top of an Order class to demonstrate the Strategy pattern, where a concrete strategy is named a promotion, and it must be a callable that accepts an Order argument:

@dataclass
class Order:  # the Context
    customer: Customer
    cart: Sequence[LineItem]
    promotion: Optional[Callable[['Order'], float]] = None  # the Strategy

Mypy 0.910 accepts the code above, but crashes with the code below:

class Order(NamedTuple):
    customer: Customer
    cart: Sequence[LineItem]
    promotion: Optional[Callable[['Order'], float]] = None  # the Strategy

I put # type: ignore instead of # the Strategy on that line, but it did not help: Mypy still crashed.

The error:

$ mypy strategy_namedtuple.py 
Deferral trace:
    strategy_namedtuple:49
    strategy_namedtuple:49
    strategy_namedtuple:-1
    ... repeat those 3 lines many times ...
strategy_namedtuple.py: error: INTERNAL ERROR: maximum semantic analysis iteration count reached
strategy_namedtuple.py:71: error: Cannot resolve name "Order" (possible cyclic definition)
strategy_namedtuple.py:76: error: Cannot resolve name "Order" (possible cyclic definition)
strategy_namedtuple.py:85: error: Cannot resolve name "Order" (possible cyclic definition)
strategy_namedtuple.py: error: INTERNAL ERROR -- Please try using mypy master on Github:
https://mypy.readthedocs.io/en/stable/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.910
Traceback (most recent call last):
  File "/Users/luciano/flupy/.py310b3/bin/mypy", line 8, in <module>
    sys.exit(console_entry())
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/__main__.py", line 11, in console_entry
    main(None, sys.stdout, sys.stderr)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/main.py", line 87, in main
    res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/main.py", line 165, in run_build
    res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/build.py", line 179, in build
    result = _build(
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/build.py", line 254, in _build
    graph = dispatch(sources, manager, stdout)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/build.py", line 2697, in dispatch
    process_graph(graph, manager)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/build.py", line 3021, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/build.py", line 3113, in process_stale_scc
    mypy.semanal_main.semantic_analysis_for_scc(graph, scc, manager.errors)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/semanal_main.py", line 84, in semantic_analysis_for_scc
    check_type_arguments(graph, scc, errors)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/semanal_main.py", line 358, in check_type_arguments
    with state.wrap_context():
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/contextlib.py", line 151, in __exit__
    self.gen.throw(type, value, traceback)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/build.py", line 1955, in wrap_context
    yield
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/semanal_main.py", line 360, in check_type_arguments
    state.tree.accept(analyzer)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/nodes.py", line 305, in accept
    return visitor.visit_mypy_file(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/semanal_typeargs.py", line 39, in visit_mypy_file
    super().visit_mypy_file(o)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/traverser.py", line 37, in visit_mypy_file
    d.accept(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/nodes.py", line 950, in accept
    return visitor.visit_class_def(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/semanal_typeargs.py", line 50, in visit_class_def
    super().visit_class_def(defn)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/mixedtraverser.py", line 28, in visit_class_def
    super().visit_class_def(o)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/traverser.py", line 73, in visit_class_def
    o.defs.accept(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/nodes.py", line 1015, in accept
    return visitor.visit_block(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/semanal_typeargs.py", line 54, in visit_block
    super().visit_block(o)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/traverser.py", line 41, in visit_block
    s.accept(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/nodes.py", line 687, in accept
    return visitor.visit_func_def(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/traverser.py", line 56, in visit_func_def
    self.visit_func(o)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/semanal_typeargs.py", line 46, in visit_func
    super().visit_func(defn)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/mixedtraverser.py", line 23, in visit_func
    self.visit_optional_type(o.type)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/mixedtraverser.py", line 91, in visit_optional_type
    t.accept(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/types.py", line 1156, in accept
    return visitor.visit_callable_type(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/typetraverser.py", line 52, in visit_callable_type
    t.fallback.accept(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/types.py", line 846, in accept
    return visitor.visit_instance(self)
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/semanal_typeargs.py", line 70, in visit_instance
    for (i, arg), tvar in zip(enumerate(t.args), info.defn.type_vars):
  File "/Users/luciano/flupy/.py310b3/lib/python3.10/site-packages/mypy/nodes.py", line 2705, in __getattribute__
    raise AssertionError(object.__getattribute__(self, 'msg'))
AssertionError: fallback can't be filled out until semanal
strategy_namedtuple.py: : note: use --pdb to drop into pdb

@AlexWaygood
Copy link
Member

AlexWaygood commented Mar 26, 2022

I've added the "crash" label in addition to the "bug" label — although the original report is a bug, @ramalho's example is clearly a crash.

@JelleZijlstra
Copy link
Member

I opened #12629 to discuss the crash. This issue should be focused on the enhancement request (support for recursive namedtuples). Any crashes should get their own issue.

@mrolle45
Copy link

I believe the source of the problem is in the TypeAnalyzer.visit_unbound_type_nonoptional method:

...
            if isinstance(node, PlaceholderNode):
                if node.becomes_typeinfo:
                    # Reference to placeholder type.
                    if self.api.final_iteration:
                        self.cannot_resolve_type(t)
                        return AnyType(TypeOfAny.from_error)
                    elif self.allow_placeholder:
                        self.api.defer()
                    else:
                        self.api.record_incomplete_ref()
                    return PlaceholderType(node.fullname, self.anal_array(t.args), t.line)
...

Before the final iteration, the reference is considered incomplete. In the final iteration, it is still a placeholder, and reported as an error. Also, the type is deemed to be Any, rather than the type of the class being defined.

Why this does not happen with non-NamedTuple classes, I cannot tell. mypy can form a correct TypeInfo for the class and for each of its members.

niklasl added a commit to niklasl/mypy that referenced this issue Jun 25, 2022
Reworks `mypy.semanal_namedtuple.analyze_namedtuple_classdef` so it can
complete recursive type references on a second pass.

The accompanying test updates raise some questions:

1. Add odd error cropped up in
   `check-incremental.test::testIncrementalWithDifferentKindsOfNestedTypesWithinMethod`
   which made `lookup_fully_qualified` throw an error:

       E   AssertionError: Cannot find component 'NT1@11' for 'b.NT1@11'

   Somehow this is prevented by a call to `reveal_type` (regardless of
   value). I cannot determine whether this is caused by this change in
   itself, or if it stumbled upon a separate issue.

2. In `check-namedtuple.test::testSelfRefNT4`, reveal_type for an
   item-accessed named tuple value now returns `builtins.object` instead
   of `Any`. While it is beyond the scope of this PR to fix
   type-inference for named tuple item access in general, it is unclear
   whether this an improvement or a slight regression?
niklasl added a commit to niklasl/mypy that referenced this issue Jun 25, 2022
Reworks `mypy.semanal_namedtuple.analyze_namedtuple_classdef` so it can
complete recursive type references on a second pass.

This addresses python#9397.

The accompanying test updates raise some questions:

1. Add odd error cropped up in
   `check-incremental.test::testIncrementalWithDifferentKindsOfNestedTypesWithinMethod`
   which made `lookup_fully_qualified` throw an error:

       E   AssertionError: Cannot find component 'NT1@11' for 'b.NT1@11'

   Somehow this is prevented by a call to `reveal_type` (regardless of
   value). I cannot determine whether this is caused by this change in
   itself, or if it stumbled upon a separate issue.

2. In `check-namedtuple.test::testSelfRefNT4`, reveal_type for an
   item-accessed named tuple value now returns `builtins.object` instead
   of `Any`. While it is beyond the scope of this PR to fix
   type-inference for named tuple item access in general, it is unclear
   whether this an improvement or a slight regression?
niklasl added a commit to niklasl/mypy that referenced this issue Jun 26, 2022
This is done by letting
`mypy.semanal_namedtuple.analyze_namedtuple_classdef` complete recursive
type references in a subsequent pass.

Fixes python#9397.
@ilevkivskyi
Copy link
Member

This now works on master with --enable-recursive-aliases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong semantic-analyzer Problems that happen during semantic analysis topic-named-tuple topic-recursive-types
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants