|
10 | 10 | from rich.style import Style |
11 | 11 |
|
12 | 12 |
|
| 13 | +def _resolve_redirect(obj: griffe.Object | griffe.Alias) -> griffe.Object | griffe.Alias | None: |
| 14 | + """Follow back-compat redirections to a real class definition. |
| 15 | +
|
| 16 | + We use two patterns to preserve import paths after a schema rename: |
| 17 | + * `from .new_path import NewClass` → `griffe.Alias` |
| 18 | + * `OldName = NewClass` (module-level assignment) → `griffe.Attribute` |
| 19 | + Both are runtime-equivalent re-exports and should expose the target |
| 20 | + class's attributes for breaking-change detection. Without resolving them, |
| 21 | + griffe reports every old attribute as removed. |
| 22 | + """ |
| 23 | + visited: set[int] = set() |
| 24 | + current: griffe.Object | griffe.Alias | None = obj |
| 25 | + while current is not None and id(current) not in visited: |
| 26 | + visited.add(id(current)) |
| 27 | + if isinstance(current, griffe.Alias): |
| 28 | + try: |
| 29 | + final = current.final_target |
| 30 | + except Exception: |
| 31 | + return None |
| 32 | + if isinstance(final, griffe.Alias) or final is current: |
| 33 | + return None |
| 34 | + return final |
| 35 | + if isinstance(current, griffe.Attribute): |
| 36 | + value = current.value |
| 37 | + parent = current.parent |
| 38 | + if parent is None or value is None: |
| 39 | + return None |
| 40 | + if isinstance(value, griffe.ExprName): |
| 41 | + next_obj = parent.members.get(str(value)) |
| 42 | + if next_obj is None: |
| 43 | + return None |
| 44 | + current = next_obj |
| 45 | + continue |
| 46 | + if isinstance(value, griffe.ExprAttribute): |
| 47 | + # Qualified path like `task_group_status.TaskGroupStatus`. |
| 48 | + # Walk segment by segment, resolving any module aliases as we go. |
| 49 | + next_obj: griffe.Object | griffe.Alias | None = parent |
| 50 | + for segment in value.values: |
| 51 | + if not isinstance(segment, griffe.ExprName): |
| 52 | + continue |
| 53 | + if isinstance(next_obj, griffe.Alias): |
| 54 | + try: |
| 55 | + next_obj = next_obj.final_target |
| 56 | + except Exception: |
| 57 | + return None |
| 58 | + if next_obj is None or not hasattr(next_obj, "members"): |
| 59 | + return None |
| 60 | + next_obj = next_obj.members.get(str(segment)) |
| 61 | + if next_obj is None: |
| 62 | + return None |
| 63 | + current = next_obj |
| 64 | + continue |
| 65 | + return None |
| 66 | + return current |
| 67 | + return None |
| 68 | + |
| 69 | + |
13 | 70 | def public_members(obj: griffe.Object | griffe.Alias) -> dict[str, griffe.Object | griffe.Alias]: |
| 71 | + target = _resolve_redirect(obj) |
| 72 | + if target is not None and target is not obj: |
| 73 | + obj = target |
| 74 | + |
14 | 75 | if isinstance(obj, griffe.Alias): |
15 | | - # ignore imports for now, they're technically part of the public API |
16 | | - # but we don't have good preventative measures in place to prevent |
17 | | - # changing them |
| 76 | + # Truly opaque alias we couldn't resolve. |
18 | 77 | return {} |
19 | 78 |
|
20 | 79 | return {name: value for name, value in obj.all_members.items() if not name.startswith("_")} |
|
0 commit comments