From 6fe851e8a52f924a499f17f99c4c7363c10dd251 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 27 Jun 2025 05:24:01 +0000 Subject: [PATCH 1/6] Harden logic for when `typing_extensions` doesn't alias `TypeAliasType`. --- .../ts_conversion/python_type_to_ts_nodes.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/python/src/typechat/_internal/ts_conversion/python_type_to_ts_nodes.py b/python/src/typechat/_internal/ts_conversion/python_type_to_ts_nodes.py index 9087baff..e663be58 100644 --- a/python/src/typechat/_internal/ts_conversion/python_type_to_ts_nodes.py +++ b/python/src/typechat/_internal/ts_conversion/python_type_to_ts_nodes.py @@ -2,6 +2,7 @@ from collections import OrderedDict import inspect +import sys import typing import typing_extensions from dataclasses import MISSING, Field, dataclass @@ -79,6 +80,15 @@ class Dataclassish(Protocol): class TypeOfTypedDict(Protocol): __total__: bool +if sys.version_info >= (3, 12) and typing.TypeAliasType is not typing_extensions.TypeAliasType: + # Sometimes typing_extensions aliases TypeAliasType, + # sometimes it's its own declaration. + def is_type_alias_type(py_type: object) -> TypeGuard[TypeAliasType]: + return isinstance(py_type, typing.TypeAliasType | typing_extensions.TypeAliasType) +else: + def is_type_alias_type(py_type: object) -> TypeGuard[TypeAliasType]: + return isinstance(py_type, typing_extensions.TypeAliasType) + def is_generic(py_type: object) -> TypeGuard[GenericAliasish]: return hasattr(py_type, "__origin__") and hasattr(py_type, "__args__") @@ -88,9 +98,8 @@ def is_dataclass(py_type: object) -> TypeGuard[Dataclassish]: TypeReferenceTarget: TypeAlias = type | TypeAliasType | TypeVar | GenericAliasish - def is_python_type_or_alias(origin: object) -> TypeGuard[type | TypeAliasType]: - return isinstance(origin, TypeAliasType | type) + return isinstance(origin, type) or is_type_alias_type(origin) _KNOWN_GENERIC_SPECIAL_FORMS: frozenset[Any] = frozenset( @@ -393,7 +402,7 @@ def declare_type(py_type: object): reserve_name(py_type) return InterfaceDeclarationNode(py_type.__name__, None, "", None, []) - if isinstance(py_type, TypeAliasType): + if is_type_alias_type(py_type): type_params = [TypeParameterDeclarationNode(type_param.__name__) for type_param in py_type.__type_params__] reserve_name(py_type) From fad080ecdde1a80cb781c7c781bb5f4f7a49e926 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 27 Jun 2025 05:24:40 +0000 Subject: [PATCH 2/6] Update/add tests. --- python/tests/test_generic_alias_1.py | 1 - python/tests/test_generic_alias_2.py | 4 +--- python/tests/test_generic_alias_3.py | 20 ++++++++++++++++++++ python/tests/test_generic_alias_4.py | 23 +++++++++++++++++++++++ python/tests/test_type_alias_syntax.py | 8 ++++++++ python/tests/utilities.py | 13 ++++++++++++- 6 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 python/tests/test_generic_alias_3.py create mode 100644 python/tests/test_generic_alias_4.py create mode 100644 python/tests/test_type_alias_syntax.py diff --git a/python/tests/test_generic_alias_1.py b/python/tests/test_generic_alias_1.py index 686fbdca..7c0acc88 100644 --- a/python/tests/test_generic_alias_1.py +++ b/python/tests/test_generic_alias_1.py @@ -5,7 +5,6 @@ T = TypeVar("T", covariant=True) - class First(Generic[T], TypedDict): kind: Literal["first"] first_attr: T diff --git a/python/tests/test_generic_alias_2.py b/python/tests/test_generic_alias_2.py index 3e90bfea..dc504e11 100644 --- a/python/tests/test_generic_alias_2.py +++ b/python/tests/test_generic_alias_2.py @@ -22,7 +22,5 @@ class Nested(TypedDict): item: FirstOrSecond[str] - -def test_generic_alias1(snapshot: Any): +def test_generic_alias2(snapshot: Any): assert(python_type_to_typescript_schema(Nested) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)) - diff --git a/python/tests/test_generic_alias_3.py b/python/tests/test_generic_alias_3.py new file mode 100644 index 00000000..ee3772c1 --- /dev/null +++ b/python/tests/test_generic_alias_3.py @@ -0,0 +1,20 @@ +from typing import Any +from .utilities import check_snapshot_for_module_string_if_3_12_plus + +module_str = """ +from typing import Literal, TypedDict +class First[T](TypedDict): + kind: Literal["first"] + first_attr: T + + +class Second[T](TypedDict): + kind: Literal["second"] + second_attr: T + + +type FirstOrSecond[T] = First[T] | Second[T] +""" + +def test_generic_alias3(snapshot: Any): + check_snapshot_for_module_string_if_3_12_plus(snapshot, input_type_str="FirstOrSecond", module_str=module_str) diff --git a/python/tests/test_generic_alias_4.py b/python/tests/test_generic_alias_4.py new file mode 100644 index 00000000..ac720476 --- /dev/null +++ b/python/tests/test_generic_alias_4.py @@ -0,0 +1,23 @@ +from typing import Any +from .utilities import check_snapshot_for_module_string_if_3_12_plus + +module_str = """ +from typing import Literal, TypedDict +class First[T](TypedDict): + kind: Literal["first"] + first_attr: T + + +class Second[T](TypedDict): + kind: Literal["second"] + second_attr: T + + +type FirstOrSecond[T] = First[T] | Second[T] + +class Nested(TypedDict): + item: FirstOrSecond[str] +""" + +def test_generic_alias4(snapshot: Any): + check_snapshot_for_module_string_if_3_12_plus(snapshot, input_type_str="Nested", module_str=module_str) diff --git a/python/tests/test_type_alias_syntax.py b/python/tests/test_type_alias_syntax.py new file mode 100644 index 00000000..4f4c19a9 --- /dev/null +++ b/python/tests/test_type_alias_syntax.py @@ -0,0 +1,8 @@ +from typing import Any +from typechat import python_type_to_typescript_schema +from .utilities import check_snapshot_for_module_string_if_3_12_plus + +module_str = "type StrOrInt = str | int" + +def test_type_alias_union1(snapshot: Any): + check_snapshot_for_module_string_if_3_12_plus(snapshot, "StrOrInt", module_str) diff --git a/python/tests/utilities.py b/python/tests/utilities.py index 427d8603..e0c1a238 100644 --- a/python/tests/utilities.py +++ b/python/tests/utilities.py @@ -1,5 +1,6 @@ from pathlib import Path import sys +import types from typing_extensions import Any, override import pytest @@ -7,7 +8,7 @@ from syrupy.extensions.single_file import SingleFileSnapshotExtension, WriteMode from syrupy.location import PyTestLocation -from typechat._internal.ts_conversion import TypeScriptSchemaConversionResult +from typechat._internal.ts_conversion import TypeScriptSchemaConversionResult, python_type_to_typescript_schema class TypeScriptSchemaSnapshotExtension(SingleFileSnapshotExtension): _write_mode = WriteMode.TEXT @@ -41,6 +42,16 @@ def dirname(cls, *, test_location: PyTestLocation) -> str: ) return str(result) +def check_snapshot_for_module_string_if_3_12_plus(snapshot: Any, input_type_str: str, module_str: str): + if sys.version_info < (3, 12): + pytest.skip("requires python 3.12 or higher") + + module = types.ModuleType("test_module") + exec(module_str, module.__dict__) + type_obj = eval(input_type_str, globals(), module.__dict__) + + assert(python_type_to_typescript_schema(type_obj) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)) + @pytest.fixture def snapshot_schema(snapshot: Any): return snapshot.with_defaults(extension_class=TypeScriptSchemaSnapshotExtension) From 6625d39efc932dcade6b368a1e29fdab3842ae0c Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 27 Jun 2025 05:24:51 +0000 Subject: [PATCH 3/6] Update snapshots. --- ...ema.d.ts => test_generic_alias2.schema.d.ts} | 0 .../test_generic_alias3.schema.d.ts | 13 +++++++++++++ .../test_generic_alias4.schema.d.ts | 17 +++++++++++++++++ .../test_type_alias_union1.schema.d.ts | 3 +++ 4 files changed, 33 insertions(+) rename python/tests/__snapshots__/test_generic_alias_2/{test_generic_alias1.schema.d.ts => test_generic_alias2.schema.d.ts} (100%) create mode 100644 python/tests/__snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts create mode 100644 python/tests/__snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts create mode 100644 python/tests/__snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts diff --git a/python/tests/__snapshots__/test_generic_alias_2/test_generic_alias1.schema.d.ts b/python/tests/__snapshots__/test_generic_alias_2/test_generic_alias2.schema.d.ts similarity index 100% rename from python/tests/__snapshots__/test_generic_alias_2/test_generic_alias1.schema.d.ts rename to python/tests/__snapshots__/test_generic_alias_2/test_generic_alias2.schema.d.ts diff --git a/python/tests/__snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts b/python/tests/__snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts new file mode 100644 index 00000000..9f7545b5 --- /dev/null +++ b/python/tests/__snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts @@ -0,0 +1,13 @@ +// Entry point is: 'FirstOrSecond' + +type FirstOrSecond = First | Second + +interface Second { + kind: "second"; + second_attr: T; +} + +interface First { + kind: "first"; + first_attr: T; +} diff --git a/python/tests/__snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts b/python/tests/__snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts new file mode 100644 index 00000000..d2d3c6f8 --- /dev/null +++ b/python/tests/__snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts @@ -0,0 +1,17 @@ +// Entry point is: 'Nested' + +interface Nested { + item: FirstOrSecond; +} + +type FirstOrSecond = First | Second + +interface Second { + kind: "second"; + second_attr: T; +} + +interface First { + kind: "first"; + first_attr: T; +} diff --git a/python/tests/__snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts b/python/tests/__snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts new file mode 100644 index 00000000..a3fd4f2b --- /dev/null +++ b/python/tests/__snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts @@ -0,0 +1,3 @@ +// Entry point is: 'StrOrInt' + +type StrOrInt = string | number From f2c3f2326509975262f83cfbdfef4a1295582f8d Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 27 Jun 2025 05:30:07 +0000 Subject: [PATCH 4/6] Remove unused import. --- python/tests/test_type_alias_syntax.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/tests/test_type_alias_syntax.py b/python/tests/test_type_alias_syntax.py index 4f4c19a9..28a0bd92 100644 --- a/python/tests/test_type_alias_syntax.py +++ b/python/tests/test_type_alias_syntax.py @@ -1,5 +1,4 @@ from typing import Any -from typechat import python_type_to_typescript_schema from .utilities import check_snapshot_for_module_string_if_3_12_plus module_str = "type StrOrInt = str | int" From 5960e94626acb11facb26fb33758c15df9400d84 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 27 Jun 2025 05:41:15 +0000 Subject: [PATCH 5/6] Create a directory for tests that can only run in 3.12+ so that unused snapshots aren't detected in 3.11. --- python/tests/utilities.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/tests/utilities.py b/python/tests/utilities.py index e0c1a238..174757b2 100644 --- a/python/tests/utilities.py +++ b/python/tests/utilities.py @@ -42,6 +42,9 @@ def dirname(cls, *, test_location: PyTestLocation) -> str: ) return str(result) +class PyVersioned3_12_PlusSnapshotExtension(PyVersionedTypeScriptSchemaSnapshotExtension): + py_ver_dir: str = f"__py3.12+_snapshots__" + def check_snapshot_for_module_string_if_3_12_plus(snapshot: Any, input_type_str: str, module_str: str): if sys.version_info < (3, 12): pytest.skip("requires python 3.12 or higher") @@ -50,7 +53,7 @@ def check_snapshot_for_module_string_if_3_12_plus(snapshot: Any, input_type_str: exec(module_str, module.__dict__) type_obj = eval(input_type_str, globals(), module.__dict__) - assert(python_type_to_typescript_schema(type_obj) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)) + assert(python_type_to_typescript_schema(type_obj) == snapshot(extension_class=PyVersioned3_12_PlusSnapshotExtension)) @pytest.fixture def snapshot_schema(snapshot: Any): From 96b80d65b460a49c390b1ac1830ceec75452be4d Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 27 Jun 2025 05:41:24 +0000 Subject: [PATCH 6/6] Update snapshots. --- .../test_generic_alias_3/test_generic_alias3.schema.d.ts | 0 .../test_generic_alias_4/test_generic_alias4.schema.d.ts | 0 .../test_type_alias_syntax/test_type_alias_union1.schema.d.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename python/tests/{__snapshots__ => __py3.12+_snapshots__}/test_generic_alias_3/test_generic_alias3.schema.d.ts (100%) rename python/tests/{__snapshots__ => __py3.12+_snapshots__}/test_generic_alias_4/test_generic_alias4.schema.d.ts (100%) rename python/tests/{__snapshots__ => __py3.12+_snapshots__}/test_type_alias_syntax/test_type_alias_union1.schema.d.ts (100%) diff --git a/python/tests/__snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts b/python/tests/__py3.12+_snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts similarity index 100% rename from python/tests/__snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts rename to python/tests/__py3.12+_snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts diff --git a/python/tests/__snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts b/python/tests/__py3.12+_snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts similarity index 100% rename from python/tests/__snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts rename to python/tests/__py3.12+_snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts diff --git a/python/tests/__snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts b/python/tests/__py3.12+_snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts similarity index 100% rename from python/tests/__snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts rename to python/tests/__py3.12+_snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts