Package: src:strawberry-graphql
Version: 0.320.0-2
User: [email protected]
Usertags: python3.15
Tags: patch, ftbfs, forky, sid
Severity: important

Hi!

While rebuilding the python related packages against the Python 3.15rc2 version 
we found that strawberry-graphql fails to build from source [1].

The error is:
E   AttributeError: module 'dataclasses' has no attribute '_is_type'

To fix it I had to backport upstream commit a0ce7fc "Add Python 3.15 test coverage 
(#4565)" [2].

I've applied the upstream fix in the sandbox [3] to verify that it builds 
successfully, please consider applying the patch to support the upcoming 3.15 
version.

Setting the severity to important for now. Once Python 3.15 is released, it 
will be added to python3-defaults and this bug will become release critical.

Happy hacking,

[1]: 
https://debusine.debian.net/debian/r-python-python3.15/work-request/1313032/
[2]: 
https://github.com/strawberry-graphql/strawberry/commit/a0ce7fce5f968043812dabdaa4ad99fedfc70086
[3]: https://debusine.debian.net/debian/r-python-python3.15/

--
"Can you imagine what I would do if I could do all I can?" -- Sun Tzu
Saludos /\/\ /\ >< `/
From: Patrick Arminio <[email protected]>
Date: Fri, 28 Aug 2026 20:21:19 +0200
Subject: Add Python 3.15 test coverage (#4565)

* Add Python 3.15 test coverage

* Install Python 3.15 build dependencies

* Complete Python 3.15 build toolchain

* Skip codeflash on Python 3.15

* Avoid private dataclasses APIs

* Preserve ClassVar detection across Python versions

---------

Co-authored-by: Amp <[email protected]>
---
 strawberry/utils/typing.py              | 33 ++++++++++++++++++++-------------
 tests/relay/test_types.py               | 13 +++++++++++++
 tests/utils/test_typing_forward_refs.py | 11 +++++++++++
 3 files changed, 44 insertions(+), 13 deletions(-)

diff --git a/strawberry/utils/typing.py b/strawberry/utils/typing.py
index a5e2886..a0c02c0 100644
--- a/strawberry/utils/typing.py
+++ b/strawberry/utils/typing.py
@@ -1,5 +1,5 @@
 import ast
-import dataclasses
+import re
 import sys
 import typing
 from collections.abc import AsyncGenerator
@@ -22,6 +22,8 @@ from typing import (  # type: ignore
     get_origin,
 )
 
+_CLASSVAR_RE = re.compile(r"^\s*(\w+(?:\s*\.\s*\w+)*)")
+
 
 @lru_cache
 def get_generic_alias(type_: type) -> type:
@@ -144,24 +146,29 @@ def is_type_var(annotation: type) -> bool:
     return isinstance(annotation, TypeVar)
 
 
-def is_classvar(cls: type, annotation: ForwardRef | str) -> bool:
+def is_classvar(cls: type, annotation: object) -> bool:
     """Returns True if the annotation is a ClassVar."""
-    # This code was copied from the dataclassses cpython implementation to check
-    # if a field is annotated with ClassVar or not, taking future annotations
-    # in consideration.
-    if dataclasses._is_classvar(annotation, typing):  # type: ignore
+    if annotation is ClassVar or get_origin(annotation) is ClassVar:
         return True
 
     annotation_str = (
         annotation.__forward_arg__ if isinstance(annotation, ForwardRef) else annotation
     )
-    return isinstance(annotation_str, str) and dataclasses._is_type(  # type: ignore
-        annotation_str,
-        cls,
-        typing,
-        typing.ClassVar,
-        dataclasses._is_classvar,  # type: ignore
-    )
+    module = sys.modules.get(cls.__module__)
+    if not isinstance(annotation_str, str) or module is None:
+        return False
+
+    match = _CLASSVAR_RE.match(annotation_str)
+    if match is None:
+        return False
+
+    resolved: object | None = module
+    for path_item in match.group(1).split("."):
+        resolved = getattr(resolved, path_item.strip(), None)
+        if resolved is None:
+            return False
+
+    return resolved is ClassVar or get_origin(resolved) is ClassVar
 
 
 def type_has_annotation(type_: object, annotation: type) -> bool:
diff --git a/tests/relay/test_types.py b/tests/relay/test_types.py
index c445cf0..aa329f2 100644
--- a/tests/relay/test_types.py
+++ b/tests/relay/test_types.py
@@ -1,3 +1,4 @@
+import typing_extensions
 from collections.abc import AsyncGenerator, AsyncIterable
 from typing import Any, Optional, Union, cast
 from typing_extensions import assert_type
@@ -259,6 +260,18 @@ def test_overwrite_resolve_id_and_no_node_id():
     strawberry.Schema(query=Query)
 
 
+def test_resolve_id_attr_ignores_qualified_classvar():
+    @strawberry.type
+    class Fruit(relay.Node):
+        id: relay.NodeID[int]
+
+    Fruit.__annotations__["registry"] = (
+        f"{typing_extensions.__name__}.ClassVar[MissingType]"
+    )
+
+    assert Fruit.resolve_id_attr() == "id"
+
+
 def test_list_connection_without_edges_or_page_info(mocker: MagicMock):
     @strawberry.type(name="Connection", description="A connection to a list of items.")
     class DummyListConnectionWithTotalCount(relay.ListConnection[relay.NodeType]):
diff --git a/tests/utils/test_typing_forward_refs.py b/tests/utils/test_typing_forward_refs.py
index 7b8bb51..41f5e56 100644
--- a/tests/utils/test_typing_forward_refs.py
+++ b/tests/utils/test_typing_forward_refs.py
@@ -1,11 +1,17 @@
 from __future__ import annotations
 
 import typing
+import typing_extensions
+from types import SimpleNamespace
 from typing import ClassVar, ForwardRef, Optional, Union
 
 from strawberry.scalars import JSON
 from strawberry.utils.typing import eval_type, is_classvar
 
+_classvar_proxy = SimpleNamespace(
+    nested=SimpleNamespace(ClassVar=ClassVar),
+)
+
 
 def test_eval_type():
     class Foo: ...
@@ -51,7 +57,12 @@ def test_is_classvar():
         attr1: str
         attr2: ClassVar[str]
         attr3: typing.ClassVar[str]
+        attr4: typing_extensions.ClassVar[str]
+
+    Foo.__annotations__["attr5"] = "_classvar_proxy.nested.ClassVar[str]"
 
     assert not is_classvar(Foo, Foo.__annotations__["attr1"])
     assert is_classvar(Foo, Foo.__annotations__["attr2"])
     assert is_classvar(Foo, Foo.__annotations__["attr3"])
+    assert is_classvar(Foo, Foo.__annotations__["attr4"])
+    assert is_classvar(Foo, Foo.__annotations__["attr5"])

Reply via email to