From c188a95b823e876f89ba9046df2cb06348f92459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 1 Apr 2023 15:53:47 +0200 Subject: [PATCH] fix: Prevent cyclic aliases by not overwriting a module member with an indirect alias to itself Issue #122: https://github.com/mkdocstrings/griffe/issues/122 --- src/griffe/dataclasses.py | 14 ++++++++++++++ src/griffe/loader.py | 14 +++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/griffe/dataclasses.py b/src/griffe/dataclasses.py index 62165b59..a83b5195 100644 --- a/src/griffe/dataclasses.py +++ b/src/griffe/dataclasses.py @@ -1111,6 +1111,20 @@ def target(self, value: Object | Alias) -> None: if self.parent is not None: self._target.aliases[self.path] = self + @property + def final_target(self) -> Object: + """Resolve and return the final target, if possible. + + This will iterate through the targets until a non-alias object is found. + + Returns: + The final target. + """ + target = self.target + while target.is_alias: + target = target.target # type: ignore[union-attr] + return target # type: ignore[return-value] + def resolve_target(self) -> None: """Resolve the target. diff --git a/src/griffe/loader.py b/src/griffe/loader.py index 875683bd..8b16a448 100644 --- a/src/griffe/loader.py +++ b/src/griffe/loader.py @@ -13,6 +13,7 @@ from __future__ import annotations import sys +from contextlib import suppress from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Sequence, cast from warnings import warn @@ -296,13 +297,24 @@ def expand_wildcards( old_lineno = getattr(old_member, "alias_lineno", old_member.lineno or 0) overwrite = alias_lineno > old_lineno # type: ignore[operator] if not self_alias and (not already_present or overwrite): - obj[new_member.name] = Alias( + alias = Alias( new_member.name, new_member, lineno=alias_lineno, endlineno=alias_endlineno, parent=obj, # type: ignore[arg-type] ) + if already_present: + prev_member = obj[new_member.name] + with suppress(AliasResolutionError): + if prev_member.is_module: + if prev_member.is_alias: + prev_member = prev_member.final_target + if alias.final_target is prev_member: + # alias named after the module it targets: + # skip to avoid cyclic aliases + continue + obj[new_member.name] = alias def resolve_module_aliases( self,