Skip to content

Commit 77e60bb

Browse files
committed
Fix pylint-dev#2628 by adding ignore_duplicate parameter
1 parent e380fd1 commit 77e60bb

File tree

4 files changed

+50
-8
lines changed

4 files changed

+50
-8
lines changed

astroid/brain/brain_dataclasses.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def _find_arguments_from_base_classes(
171171
# See TODO down below
172172
# all_have_defaults = True
173173

174-
for base in reversed(node.mro()):
174+
for base in reversed(node.mro(ignore_duplicates=True)):
175175
if not base.is_dataclass:
176176
continue
177177
try:
@@ -221,7 +221,7 @@ def _parse_arguments_into_strings(
221221

222222
def _get_previous_field_default(node: nodes.ClassDef, name: str) -> nodes.NodeNG | None:
223223
"""Get the default value of a previously defined field."""
224-
for base in reversed(node.mro()):
224+
for base in reversed(node.mro(ignore_duplicates=True)):
225225
if not base.is_dataclass:
226226
continue
227227
if name in base.locals:

astroid/nodes/scoped_nodes/scoped_nodes.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,13 @@ def clean_duplicates_mro(
144144
sequences: list[list[ClassDef]],
145145
cls: ClassDef,
146146
context: InferenceContext | None,
147+
ignore_duplicates: bool,
147148
) -> list[list[ClassDef]]:
148149
for sequence in sequences:
149150
seen = set()
150151
for node in sequence:
151152
lineno_and_qname = (node.lineno, node.qname())
152-
if lineno_and_qname in seen:
153+
if lineno_and_qname in seen and not ignore_duplicates:
153154
raise DuplicateBasesError(
154155
message="Duplicates found in MROs {mros} for {cls!r}.",
155156
mros=sequences,
@@ -2834,7 +2835,9 @@ def _inferred_bases(self, context: InferenceContext | None = None):
28342835
else:
28352836
yield from baseobj.bases
28362837

2837-
def _compute_mro(self, context: InferenceContext | None = None):
2838+
def _compute_mro(
2839+
self, context: InferenceContext | None = None, ignore_duplicates: bool = False
2840+
):
28382841
if self.qname() == "builtins.object":
28392842
return [self]
28402843

@@ -2844,23 +2847,29 @@ def _compute_mro(self, context: InferenceContext | None = None):
28442847
if base is self:
28452848
continue
28462849

2847-
mro = base._compute_mro(context=context)
2850+
mro = base._compute_mro(
2851+
context=context, ignore_duplicates=ignore_duplicates
2852+
)
28482853
bases_mro.append(mro)
28492854

28502855
unmerged_mro: list[list[ClassDef]] = [[self], *bases_mro, inferred_bases]
2851-
unmerged_mro = clean_duplicates_mro(unmerged_mro, self, context)
2856+
unmerged_mro = clean_duplicates_mro(
2857+
unmerged_mro, self, context, ignore_duplicates=ignore_duplicates
2858+
)
28522859
clean_typing_generic_mro(unmerged_mro)
28532860
return _c3_merge(unmerged_mro, self, context)
28542861

2855-
def mro(self, context: InferenceContext | None = None) -> list[ClassDef]:
2862+
def mro(
2863+
self, context: InferenceContext | None = None, ignore_duplicates: bool = False
2864+
) -> list[ClassDef]:
28562865
"""Get the method resolution order, using C3 linearization.
28572866
28582867
:returns: The list of ancestors, sorted by the mro.
28592868
:rtype: list(NodeNG)
28602869
:raises DuplicateBasesError: Duplicate bases in the same class base
28612870
:raises InconsistentMroError: A class' MRO is inconsistent
28622871
"""
2863-
return self._compute_mro(context=context)
2872+
return self._compute_mro(context=context, ignore_duplicates=ignore_duplicates)
28642873

28652874
def bool_value(self, context: InferenceContext | None = None) -> Literal[True]:
28662875
"""Determine the boolean value of this node.

tests/brain/test_dataclasses.py

+30
Original file line numberDiff line numberDiff line change
@@ -1350,3 +1350,33 @@ def attr(self, value: int) -> None:
13501350
fourth_init: bases.UnboundMethod = next(fourth.infer())
13511351
assert [a.name for a in fourth_init.args.args] == ["self", "other_attr", "attr"]
13521352
assert [a.name for a in fourth_init.args.defaults] == ["Uninferable"]
1353+
1354+
1355+
@parametrize_module
1356+
def test_dataclass_inherited_from_multiple_protocol_bases(module: str):
1357+
code = astroid.extract_node(
1358+
f"""
1359+
from {module} import dataclass
1360+
from typing import TypeVar, Protocol
1361+
1362+
BaseT = TypeVar("BaseT")
1363+
T = TypeVar("T", bound=BaseT)
1364+
1365+
1366+
class A(Protocol[BaseT]):
1367+
pass
1368+
1369+
1370+
class B(A[T], Protocol[T]):
1371+
pass
1372+
1373+
1374+
@dataclass
1375+
class Dataclass(B[T]):
1376+
pass
1377+
"""
1378+
)
1379+
inferred = code.inferred()
1380+
assert len(inferred) == 1
1381+
assert isinstance(inferred[0], nodes.ClassDef)
1382+
assert inferred[0].is_dataclass

tests/test_scoped_nodes.py

+3
Original file line numberDiff line numberDiff line change
@@ -1938,6 +1938,7 @@ class A(Generic[T1], Generic[T2]): ...
19381938
assert isinstance(cls, nodes.ClassDef)
19391939
with self.assertRaises(DuplicateBasesError):
19401940
cls.mro()
1941+
assert len(cls.mro(ignore_duplicates=True)) == 3
19411942

19421943
def test_mro_generic_error_2(self):
19431944
cls = builder.extract_node(
@@ -1951,6 +1952,8 @@ class B(A[T], A[T]): ...
19511952
assert isinstance(cls, nodes.ClassDef)
19521953
with self.assertRaises(DuplicateBasesError):
19531954
cls.mro()
1955+
with self.assertRaises(InconsistentMroError):
1956+
cls.mro(ignore_duplicates=True)
19541957

19551958
def test_mro_typing_extensions(self):
19561959
"""Regression test for mro() inference on typing_extensions.

0 commit comments

Comments
 (0)