Skip to content

Commit 05578e9

Browse files
Add Pytest step to CI (#209)
* Add Pytest step to CI * Only use `__type_params__` if it's non-empty so that old-style `Generic`s are preserved in 3.12+. * Update snapshots. * Update snapshot tests to specify a custom extension class. * Update snapshots. * Add a custom SnapshotExtension for running certain tests in a different folder depending on the tested Python version. * Update snapshots. * Handle 'dict' as a base for older versions of Python. * Update snapshots.
1 parent 4d69ffa commit 05578e9

29 files changed

Lines changed: 350 additions & 46 deletions

.github/workflows/ci.python.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,8 @@ jobs:
6060
python-version: ${{ matrix.python-version}}
6161
annotate: ${{ matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest' }} # Only let one build post comments.
6262
working-directory: ./python
63+
64+
- name: Test with Pytest
65+
run: |
66+
pytest -vv
67+

python/src/typechat/_internal/ts_conversion/python_type_to_ts_nodes.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,16 @@ def is_python_type_or_alias(origin: object) -> TypeGuard[type | TypeAliasType]:
104104
]
105105
)
106106

107-
_KNOWN_SPECIAL_BASES: frozenset[Any] = frozenset([typing.TypedDict, typing_extensions.TypedDict, Protocol])
107+
_KNOWN_SPECIAL_BASES: frozenset[Any] = frozenset([
108+
typing.TypedDict,
109+
typing_extensions.TypedDict,
110+
Protocol,
111+
112+
# In older versions of Python, `__orig_bases__` will not be defined on `TypedDict`s
113+
# derived from the built-in `typing` module (but they will from `typing_extensions`!).
114+
# So `get_original_bases` will fetch `__bases__` which will map `TypedDict` to a plain `dict`.
115+
dict,
116+
])
108117

109118

110119
@dataclass
@@ -327,12 +336,12 @@ def declare_type(py_type: object):
327336
if (is_typeddict(py_type) or is_dataclass(py_type)) and isinstance(py_type, type):
328337
comment = py_type.__doc__ or ""
329338

330-
if hasattr(py_type, "__type_params__"):
339+
if hasattr(py_type, "__type_params__") and cast(GenericDeclarationish, py_type).__type_params__:
331340
type_params = [
332341
TypeParameterDeclarationNode(type_param.__name__)
333342
for type_param in cast(GenericDeclarationish, py_type).__type_params__
334343
]
335-
elif hasattr(py_type, "__parameters__"):
344+
elif hasattr(py_type, "__parameters__") and cast(GenericDeclarationish, py_type).__parameters__:
336345
type_params = [
337346
TypeParameterDeclarationNode(type_param.__name__)
338347
for type_param in cast(GenericDeclarationish, py_type).__parameters__
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Entry point is: 'Derived'
2+
3+
interface Derived {
4+
"my_attr_1": string;
5+
"my_attr_2": number;
6+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Entry point is: 'D_or_E'
2+
3+
type D_or_E = D | E
4+
5+
// This is the definition of the class E.
6+
interface E extends C<string> {
7+
tag: "E";
8+
next: this | null;
9+
}
10+
11+
// This is a generic class named C.
12+
interface C<T> {
13+
x?: T;
14+
c: C<number | null>;
15+
}
16+
17+
// This is the definition of the class D.
18+
interface D extends C<string> {
19+
tag?: "D";
20+
// This comes from string metadata
21+
// within an Annotated hint.
22+
y: boolean | null;
23+
z?: number[] | null;
24+
other?: IndirectC;
25+
"non_class"?: NonClass;
26+
// This comes from later metadata.
27+
"multiple_metadata"?: string;
28+
}
29+
30+
interface NonClass {
31+
a: number;
32+
"my-dict": Record<string, number>;
33+
}
34+
35+
type IndirectC = C<number>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Entry point is: 'Derived'
2+
3+
// ERRORS:
4+
// !!! Cannot create a schema using two types with the same name. C conflicts between <class 'tests.test_conflicting_names_1.a.<locals>.C'> and <class 'tests.test_conflicting_names_1.b.<locals>.C'>
5+
6+
interface Derived extends C, C {
7+
}
8+
9+
interface C {
10+
"my_attr_2": number;
11+
}
12+
13+
interface C {
14+
"my_attr_1": string;
15+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Entry point is: 'D_or_E'
2+
3+
type D_or_E = D | E
4+
5+
// This is the definition of the class E.
6+
interface E extends C<string> {
7+
tag: "E";
8+
next: this | null;
9+
}
10+
11+
// This is a generic class named C.
12+
interface C<T> {
13+
x?: T;
14+
c: C<number | null>;
15+
}
16+
17+
// This is the definition of the class D.
18+
interface D extends C<string> {
19+
tag?: "D";
20+
// This comes from string metadata
21+
// within an Annotated hint.
22+
y: boolean | null;
23+
z?: number[] | null;
24+
other?: IndirectC;
25+
"non_class"?: NonClass;
26+
// This comes from later metadata.
27+
"multiple_metadata"?: string;
28+
}
29+
30+
interface NonClass {
31+
a: number;
32+
"my-dict": Record<string, number>;
33+
}
34+
35+
type IndirectC = C<number>

python/tests/__snapshots__/test_coffeeshop.ambr

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Entry point is: 'Cart'
2+
3+
interface Cart {
4+
type: "Cart";
5+
items: Array<LineItem | UnknownText>;
6+
}
7+
8+
// Represents any text that could not be understood.
9+
interface UnknownText {
10+
type: "UnknownText";
11+
// The text that wasn't understood
12+
text: string;
13+
}
14+
15+
interface LineItem {
16+
type: "LineItem";
17+
product: BakeryProduct | LatteDrink | CoffeeDrink | EspressoDrink | UnknownText;
18+
quantity: number;
19+
}
20+
21+
interface EspressoDrink {
22+
type: "EspressoDrink";
23+
name: "espresso" | "lungo" | "ristretto" | "macchiato";
24+
temperature?: "hot" | "extra hot" | "warm" | "iced";
25+
// The default is 'doppio'
26+
size?: "solo" | "doppio" | "triple" | "quad";
27+
options?: Array<Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation>;
28+
}
29+
30+
interface LattePreparation {
31+
type: "LattePreparation";
32+
name: "for here cup" | "lid" | "with room" | "to go" | "dry" | "wet";
33+
}
34+
35+
interface Caffeine {
36+
type: "Caffeine";
37+
name: "regular" | "two thirds caf" | "half caf" | "one third caf" | "decaf";
38+
}
39+
40+
interface Topping {
41+
type: "Topping";
42+
name: "cinnamon" | "foam" | "ice" | "nutmeg" | "whipped cream" | "water";
43+
optionQuantity?: "no" | "light" | "regular" | "extra";
44+
}
45+
46+
interface Syrup {
47+
type: "Syrup";
48+
name: "almond syrup" | "buttered rum syrup" | "caramel syrup" | "cinnamon syrup" | "hazelnut syrup" | "orange syrup" | "peppermint syrup" | "raspberry syrup" | "toffee syrup" | "vanilla syrup";
49+
optionQuantity?: "no" | "light" | "regular" | "extra";
50+
}
51+
52+
interface Sweetener {
53+
type: "Sweetener";
54+
name: "equal" | "honey" | "splenda" | "sugar" | "sugar in the raw" | "sweet n low" | "espresso shot";
55+
optionQuantity?: "no" | "light" | "regular" | "extra";
56+
}
57+
58+
interface Creamer {
59+
type: "Creamer";
60+
name: "whole milk creamer" | "two percent milk creamer" | "one percent milk creamer" | "nonfat milk creamer" | "coconut milk creamer" | "soy milk creamer" | "almond milk creamer" | "oat milk creamer" | "half and half" | "heavy cream";
61+
}
62+
63+
interface CoffeeDrink {
64+
type: "CoffeeDrink";
65+
name: "americano" | "coffee";
66+
temperature?: "hot" | "extra hot" | "warm" | "iced";
67+
// The default is 'grande'
68+
size?: "short" | "tall" | "grande" | "venti";
69+
options?: Array<Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation>;
70+
}
71+
72+
interface LatteDrink {
73+
type: "LatteDrink";
74+
name: "cappuccino" | "flat white" | "latte" | "latte macchiato" | "mocha" | "chai latte";
75+
temperature?: "hot" | "extra hot" | "warm" | "iced";
76+
// The default is 'grande'
77+
size?: "short" | "tall" | "grande" | "venti";
78+
options?: Array<Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation>;
79+
}
80+
81+
interface BakeryProduct {
82+
type: "BakeryProduct";
83+
name: "apple bran muffin" | "blueberry muffin" | "lemon poppyseed muffin" | "bagel";
84+
options?: Array<BakeryOption | BakeryPreparation>;
85+
}
86+
87+
interface BakeryPreparation {
88+
type: "BakeryPreparation";
89+
name: "warmed" | "cut in half";
90+
}
91+
92+
interface BakeryOption {
93+
type: "BakeryOption";
94+
name: "butter" | "strawberry jam" | "cream cheese";
95+
optionQuantity?: "no" | "light" | "regular" | "extra";
96+
}

python/tests/__snapshots__/test_conflicting_names_1.ambr

Lines changed: 0 additions & 4 deletions
This file was deleted.

python/tests/__snapshots__/test_dataclasses.ambr

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)