Skip to content

Callable types are not handled correctly in frozen dataclasses #12312

@uSpike

Description

@uSpike
Contributor

Bug Report

Using a frozen dataclass with an attribute typed as a TypeAlias for a Callable results in mypy treating the attribute as the return value. See reproduce for more easy to understand example.

To Reproduce

from dataclasses import dataclass
from typing import Callable, TypeAlias

Func: TypeAlias = Callable[..., None]

@dataclass(frozen=True, eq=False)
class Foo:
    bar: Func

def example() -> None:
    pass

Foo(bar=example).bar()

results in

test.py:13:1: error: "None" not callable  [misc]
Found 1 error in 1 file (checked 1 source file)

Expected Behavior

mypy should pass

Actual Behavior

Removing frozen=True causes mypy to pass this code.

Your Environment

  • Mypy version used: 0.931
  • Mypy command-line flags: N/A
  • Mypy configuration options from mypy.ini (and other config files):
python_version=3.10
platform=linux
plugins=pydantic.mypy, sqlalchemy.ext.mypy.plugin

show_column_numbers=True
show_error_codes=True

# show error messages from unrelated files
follow_imports=normal

# show errors about unsatisfied imports
ignore_missing_imports = False

# be strict
disallow_untyped_calls=True
warn_return_any=True
strict_optional=True
warn_no_return=True
warn_redundant_casts=True
warn_unused_ignores=True

# The following are off by default.  Flip them on if you feel
# adventurous.
disallow_untyped_defs=True
check_untyped_defs=True
  • Python version used: 3.10.2
  • Operating system and version: Ubuntu 20.04

Activity

uSpike

uSpike commented on Mar 19, 2022

@uSpike
ContributorAuthor

I have a test case that reproduces this:

diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index eed329bb5..ab2efacc8 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -1536,3 +1536,20 @@ A(a=1, b=2)
 A(1)
 A(a="foo")  # E: Argument "a" to "A" has incompatible type "str"; expected "int"
 [builtins fixtures/dataclasses.pyi]
+
+[case testDataclassesCallableTypeAlias]
+# flags: --python-version 3.7
+from dataclasses import dataclass
+from typing import Any, Callable
+from typing_extensions import TypeAlias
+alias: TypeAlias = Callable[..., None]
+@dataclass(frozen=True)
+class A:
+    a: alias
+
+def func() -> None:
+    pass
+
+reveal_type(A.a)  # N: Revealed type is "def (*Any, **Any)"
+A(a=func).a()
+[builtins fixtures/dataclasses.pyi]
data: /home/jordan/src/mypy/test-data/unit/check-dataclasses.test:1540:
/home/jordan/src/mypy/mypy/test/testcheck.py:140: in run_case
    self.run_case_once(testcase)
/home/jordan/src/mypy/mypy/test/testcheck.py:227: in run_case_once
    assert_string_arrays_equal(output, a, msg.format(testcase.file, testcase.line))
E   AssertionError: Unexpected type checker output (/home/jordan/src/mypy/test-data/unit/check-dataclasses.test, line 1540)
--------------------------------------------------------- Captured stderr call ----------------------------------------------------------
Expected:
  main:13: note: Revealed type is "def (*Any, **Any)"
Actual:
  main:13: note: Revealed type is "def (*Any, **Any)"
  main:14: error: "None" not callable           (diff)
uSpike

uSpike commented on Mar 19, 2022

@uSpike
ContributorAuthor

Upon further investigation, this issue is not specific to TypeAlias. Any dataclass with a Callable attribute will not work if frozen. See this failing test case:

diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index eed329bb5..cc8efb5b4 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -1536,3 +1536,18 @@ A(a=1, b=2)
 A(1)
 A(a="foo")  # E: Argument "a" to "A" has incompatible type "str"; expected "int"
 [builtins fixtures/dataclasses.pyi]
+
+[case testDataclassesCallableFrozen]
+# flags: --python-version 3.7
+from dataclasses import dataclass
+from typing import Any, Callable
+@dataclass(frozen=True)
+class A:
+    a: Callable[..., None]
+
+def func() -> None:
+    pass
+
+reveal_type(A.a)  # N: Revealed type is "def (*Any, **Any)"
+A(a=func).a()
+[builtins fixtures/dataclasses.pyi]

Here's a simple fix that i will make a PR for shortly:

diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
index 091c627f5..254491853 100644
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -211,8 +211,8 @@ class DataclassTransformer:
 
         if decorator_arguments['frozen']:
             self._freeze(attributes)
-        else:
-            self._propertize_callables(attributes)
+
+        self._propertize_callables(attributes)
 
         if decorator_arguments['slots']:
             self.add_slots(info, attributes, correct_version=py_version >= (3, 10))
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test

It seems that the dataclass plugin was not calling self._propertize_callables() if the dataclass is frozen. I'll look more into the history around that code to see if there's a reason why it was written that way, but the above change passes the entire test suite.

changed the title [-]Frozen dataclass Callable attribute cannot be a TypeAlias[/-] [+]Callable types are not handled correctly in frozen dataclasses[/+] on Mar 19, 2022
wrobell

wrobell commented on Mar 25, 2022

@wrobell

Looks like regression #10711?

uSpike

uSpike commented on Mar 25, 2022

@uSpike
ContributorAuthor

I'm not sure it's really a regression since mypy seems to have never properly supported frozen dataclasses with Callable types.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      Participants

      @wrobell@JelleZijlstra@uSpike

      Issue actions

        Callable types are not handled correctly in frozen dataclasses · Issue #12312 · python/mypy