Skip to content

Commit dcfd913

Browse files
Issue an error when schema construction encounters two types with the same name. (#204)
* Add a test for two types with the same name. * Track declarations by name. * Update snapshots.
1 parent 1bfbef6 commit dcfd913

3 files changed

Lines changed: 48 additions & 6 deletions

File tree

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def python_type_to_typescript_nodes(root_py_type: object) -> TypeScriptNodeTrans
139139

140140
declared_types: OrderedDict[object, TopLevelDeclarationNode | None] = OrderedDict()
141141
undeclared_types: OrderedDict[object, object] = OrderedDict({root_py_type: root_py_type}) # just a set, really
142+
used_names: dict[str, type | TypeAliasType] = {}
142143
errors: list[str] = []
143144

144145
def skip_annotations(py_type: object) -> object:
@@ -226,7 +227,7 @@ def convert_to_type_node(py_type: object) -> TypeNode:
226227
if key_type_arg is not NumberTypeReferenceNode:
227228
key_type_arg = StringTypeReferenceNode
228229
return TypeReferenceNode(IdentifierNode("Record"), [key_type_arg, value_type_arg])
229-
230+
230231
if origin is tuple:
231232
# Note that when the type is `tuple[()]`,
232233
# `type_args` will be an empty tuple.
@@ -246,9 +247,9 @@ def convert_to_type_node(py_type: object) -> TypeNode:
246247
f"The tuple type '{py_type}' is ill-formed because the ellipsis (...) cannot be the first element."
247248
)
248249
return ArrayTypeNode(AnyTypeReferenceNode)
249-
250+
250251
return ArrayTypeNode(convert_to_type_node(type_args[0]))
251-
252+
252253
return TupleTypeNode([convert_to_type_node(py_type_arg) for py_type_arg in type_args])
253254

254255
if origin is Union or origin is UnionType:
@@ -272,7 +273,7 @@ def convert_to_type_node(py_type: object) -> TypeNode:
272273

273274
errors.append(f"'{py_type}' cannot be used as a type annotation.")
274275
return AnyTypeReferenceNode
275-
276+
276277
def declare_property(name: str, py_annotation: type | TypeAliasType, is_typeddict_attribute: bool, optionality_default: bool):
277278
"""
278279
Declare a property for a given type.
@@ -315,6 +316,13 @@ def declare_property(name: str, py_annotation: type | TypeAliasType, is_typeddic
315316
type_annotation = convert_to_type_node(skip_annotations(current_annotation))
316317
return PropertyDeclarationNode(name, optional, comment or "", type_annotation)
317318

319+
def reserve_name(val: type | TypeAliasType):
320+
type_name = val.__name__
321+
if type_name in used_names:
322+
errors.append(f"Cannot create a schema using two types with the same name. {type_name} conflicts between {val} and {used_names[type_name]}")
323+
else:
324+
used_names[type_name] = val
325+
318326
def declare_type(py_type: object):
319327
if (is_typeddict(py_type) or is_dataclass(py_type)) and isinstance(py_type, type):
320328
comment = py_type.__doc__ or ""
@@ -368,13 +376,19 @@ def declare_type(py_type: object):
368376
prop = declare_property(attr_name, type_hint, is_typeddict_attribute=False, optionality_default=optional)
369377
properties.append(prop)
370378

379+
reserve_name(py_type)
371380
return InterfaceDeclarationNode(py_type.__name__, type_params, comment, bases, properties)
372381
if isinstance(py_type, type):
373-
errors.append("Currently only TypedDict, dataclass, and type alias declarations are supported in TypeChat.")
374-
return InterfaceDeclarationNode(py_type.__name__, None, f"Comment for {py_type.__name__}.", None, [])
382+
errors.append(f"{py_type.__name__} was not a TypedDict, dataclass, or type alias, and cannot be translated.")
383+
384+
reserve_name(py_type)
385+
386+
return InterfaceDeclarationNode(py_type.__name__, None, "", None, [])
375387
if isinstance(py_type, TypeAliasType):
376388
type_params = [TypeParameterDeclarationNode(type_param.__name__) for type_param in py_type.__type_params__]
377389

390+
reserve_name(py_type)
391+
378392
return TypeAliasDeclarationNode(
379393
py_type.__name__,
380394
type_params,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# serializer version: 1
2+
# name: test_conflicting_names_1
3+
TypeScriptSchemaConversionResult(typescript_schema_str='interface Derived extends C, C {\n}\n\ninterface C {\n "my_attr_2": number;\n}\n\ninterface C {\n "my_attr_1": string;\n}\n', typescript_type_reference='Derived', errors=["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'>"])
4+
# ---
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Any, TypedDict, cast
2+
3+
from typechat import python_type_to_typescript_schema
4+
5+
6+
def a():
7+
class C(TypedDict):
8+
my_attr_1: str
9+
return C
10+
11+
12+
def b():
13+
class C(TypedDict):
14+
my_attr_2: int
15+
return C
16+
17+
A = a()
18+
B = b()
19+
20+
class Derived(A, B): # type: ignore
21+
pass
22+
23+
def test_conflicting_names_1(snapshot: Any):
24+
assert python_type_to_typescript_schema(cast(type, Derived)) == snapshot

0 commit comments

Comments
 (0)