main
 1from __future__ import annotations
 2
 3import sys
 4from typing import Iterator
 5from pathlib import Path
 6
 7import rich
 8import griffe
 9from rich.text import Text
10from rich.style import Style
11
12
13def public_members(obj: griffe.Object | griffe.Alias) -> dict[str, griffe.Object | griffe.Alias]:
14    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
18        return {}
19
20    return {name: value for name, value in obj.all_members.items() if not name.startswith("_")}
21
22
23def find_breaking_changes(
24    new_obj: griffe.Object | griffe.Alias,
25    old_obj: griffe.Object | griffe.Alias,
26    *,
27    path: list[str],
28) -> Iterator[Text | str]:
29    new_members = public_members(new_obj)
30    old_members = public_members(old_obj)
31
32    for name, old_member in old_members.items():
33        if isinstance(old_member, griffe.Alias) and len(path) > 2:
34            # ignore imports in `/types/` for now, they're technically part of the public API
35            # but we don't have good preventative measures in place to prevent changing them
36            continue
37
38        new_member = new_members.get(name)
39        if new_member is None:
40            cls_name = old_member.__class__.__name__
41            yield Text(f"({cls_name})", style=Style(color="rgb(119, 119, 119)"))
42            yield from [" " for _ in range(10 - len(cls_name))]
43            yield f" {'.'.join(path)}.{name}"
44            yield "\n"
45            continue
46
47        yield from find_breaking_changes(new_member, old_member, path=[*path, name])
48
49
50def main() -> None:
51    try:
52        against_ref = sys.argv[1]
53    except IndexError as err:
54        raise RuntimeError("You must specify a base ref to run breaking change detection against") from err
55
56    package = griffe.load(
57        "openai",
58        search_paths=[Path(__file__).parent.parent.joinpath("src")],
59    )
60    old_package = griffe.load_git(
61        "openai",
62        ref=against_ref,
63        search_paths=["src"],
64    )
65    assert isinstance(package, griffe.Module)
66    assert isinstance(old_package, griffe.Module)
67
68    output = list(find_breaking_changes(package, old_package, path=["openai"]))
69    if output:
70        rich.print(Text("Breaking changes detected!", style=Style(color="rgb(165, 79, 87)")))
71        rich.print()
72
73        for text in output:
74            rich.print(text, end="")
75
76        sys.exit(1)
77
78
79main()