[Backport maintenance/4.1.x] Fix DuplicateBasesError crash in dataclass transform (#2982)
Fix DuplicateBasesError crash in dataclass transform (#2970)
(cherry picked from commit 95960c4dee94c01e0919251f5f612d162d844901)
Co-authored-by: Fridayworks <aifriday700@gmail.com>
diff --git a/ChangeLog b/ChangeLog
index 7d15bf0..b6621fa 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -13,6 +13,13 @@
============================
Release date: TBA
+* Fix ``DuplicateBasesError`` crash in dataclass transform when a class has
+ duplicate bases in its MRO (e.g., ``Protocol`` appearing both directly and
+ indirectly). Catch ``MroError`` at ``.mro()`` call sites in
+ ``brain_dataclasses.py``, consistent with the existing pattern elsewhere.
+
+ Closes #2628
+
* Catch ``MemoryError`` when inferring f-strings with extremely large format
widths (e.g. ``f'{0:11111111111}'``) so that inference yields ``Uninferable``
instead of crashing.
diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py
index 244665e..b6b1956 100644
--- a/astroid/brain/brain_dataclasses.py
+++ b/astroid/brain/brain_dataclasses.py
@@ -21,7 +21,12 @@
from astroid.brain.helpers import is_class_var
from astroid.builder import parse
from astroid.const import PY313_PLUS
-from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceDefault
+from astroid.exceptions import (
+ AstroidSyntaxError,
+ InferenceError,
+ MroError,
+ UseInferenceDefault,
+)
from astroid.inference_tip import inference_tip
from astroid.manager import AstroidManager
from astroid.typing import InferenceResult
@@ -174,7 +179,12 @@
# See TODO down below
# all_have_defaults = True
- for base in reversed(node.mro()):
+ try:
+ mro = node.mro()
+ except MroError:
+ return pos_only_store, kw_only_store
+
+ for base in reversed(mro):
if not base.is_dataclass:
continue
try:
@@ -224,7 +234,12 @@
def _get_previous_field_default(node: nodes.ClassDef, name: str) -> nodes.NodeNG | None:
"""Get the default value of a previously defined field."""
- for base in reversed(node.mro()):
+ try:
+ mro = node.mro()
+ except MroError:
+ return None
+
+ for base in reversed(mro):
if not base.is_dataclass:
continue
if name in base.locals:
diff --git a/tests/brain/test_dataclasses.py b/tests/brain/test_dataclasses.py
index d6ab13e..4795327 100644
--- a/tests/brain/test_dataclasses.py
+++ b/tests/brain/test_dataclasses.py
@@ -1237,3 +1237,73 @@
fourth_init: bases.UnboundMethod = next(fourth.infer())
assert [a.name for a in fourth_init.args.args] == ["self", "other_attr", "attr"]
assert [a.name for a in fourth_init.args.defaults] == ["Uninferable"]
+
+
+def test_dataclass_with_duplicate_bases_no_crash():
+ """Regression test for https://github.com/pylint-dev/astroid/issues/2628.
+
+ A dataclass inheriting from a class with duplicate bases in MRO
+ (e.g., Protocol appearing both directly and indirectly) should not
+ crash with DuplicateBasesError during AST transformation.
+ """
+ code = """
+ import dataclasses
+ from typing import TypeVar, Protocol
+
+ BaseT = TypeVar("BaseT")
+ T = TypeVar("T", bound=BaseT)
+
+ class ConfigBase(Protocol[BaseT]):
+ ...
+
+ class Config(ConfigBase[T], Protocol[T]):
+ ...
+
+ @dataclasses.dataclass
+ class DatasetConfig(Config[T]):
+ name: str = "default"
+
+ DatasetConfig.__init__ #@
+ """
+ node = astroid.extract_node(code)
+ # Should not raise DuplicateBasesError — graceful degradation instead
+ inferred = next(node.infer())
+ assert inferred is not None
+
+
+def test_dataclass_with_duplicate_bases_field_default():
+ """Regression test for _get_previous_field_default with broken MRO.
+
+ When a parent dataclass defines a field with a default and a child (with
+ duplicate bases in its MRO) re-annotates that field without a value,
+ _get_previous_field_default should not crash with DuplicateBasesError.
+
+ See https://github.com/pylint-dev/astroid/issues/2628.
+ """
+ code = """
+ import dataclasses
+ from typing import TypeVar, Protocol
+
+ BaseT = TypeVar("BaseT")
+ T = TypeVar("T", bound=BaseT)
+
+ class ConfigBase(Protocol[BaseT]):
+ ...
+
+ class Config(ConfigBase[T], Protocol[T]):
+ ...
+
+ @dataclasses.dataclass
+ class BaseConfig(Config[T]):
+ name: str = "default"
+
+ @dataclasses.dataclass
+ class ChildConfig(BaseConfig[T]):
+ name: str
+
+ ChildConfig.__init__ #@
+ """
+ node = astroid.extract_node(code)
+ # Should not raise DuplicateBasesError in _get_previous_field_default
+ inferred = next(node.infer())
+ assert inferred is not None